@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.
@@ -1 +1 @@
1
- {"version":3,"file":"TeamInviteMemberPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/TeamInviteMemberPagePage.tsx"],"names":[],"mappings":"AAgDA,wBAAgB,wBAAwB,4CAyJvC"}
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 t } from "../providers/PlanMagnetProvider.js";
2
- import { throwOnError as n } from "../utils/mutationResult.js";
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 { useState as i } from "react";
5
- import { Send as a, UserPlus as o, X as s } from "lucide-react";
6
- import { gql as c } from "@apollo/client";
7
- import { useMutation as l, useQuery as u } from "@apollo/client/react";
8
- import { Fragment as d, jsx as f, jsxs as p } from "react/jsx-runtime";
9
- import { Button as m, Card as h, CardContent as g, CardHeader as _, CardTitle as v, Input as y } from "@burdenoff/fe-libs/ui";
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 b = c`
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
- `, x = c`
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
- function S() {
34
- let { workspaceId: c } = e(), S = t(), C = u(b, {
35
- variables: { scopeId: c },
36
- skip: !c
37
- }), [w, T] = l(x), [E, D] = i(""), [O, k] = i(!1), [A, j] = i(""), [M, N] = i(null), [P, F] = i(null), I = C.data?.roles ?? [], L = I.find((e) => e.id === A) ?? null, R = r(E), z = R && !!L && !T.loading && !!c;
38
- return /* @__PURE__ */ p("div", {
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__ */ p("div", {
83
+ /* @__PURE__ */ v("div", {
42
84
  className: "flex items-center justify-between",
43
- children: [/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("h1", {
85
+ children: [/* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("h1", {
44
86
  className: "text-2xl font-semibold",
45
- children: "Invite Team Member"
46
- }), /* @__PURE__ */ f("p", {
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__ */ p(m, {
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: () => S("/team/team-page"),
53
- children: [/* @__PURE__ */ f(s, { className: "mr-1 h-4 w-4" }), "Back to team"]
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
- P && /* @__PURE__ */ p("div", {
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
- "Invitation sent to ",
62
- P,
63
- ". They get this access when they accept it."
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
- M && /* @__PURE__ */ f("p", {
134
+ B && /* @__PURE__ */ _("p", {
67
135
  role: "alert",
68
136
  className: "text-sm text-destructive",
69
137
  "data-testid": "invite-error",
70
- children: M
138
+ children: B
71
139
  }),
72
- /* @__PURE__ */ p(h, { children: [/* @__PURE__ */ f(_, { children: /* @__PURE__ */ p(v, {
140
+ /* @__PURE__ */ v(b, { children: [/* @__PURE__ */ _(S, { children: /* @__PURE__ */ v(C, {
73
141
  className: "flex items-center gap-2",
74
- children: [/* @__PURE__ */ f(o, { className: "h-4 w-4" }), "Invitation details"]
75
- }) }), /* @__PURE__ */ p(g, {
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__ */ p("div", { children: [
79
- /* @__PURE__ */ f("label", {
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__ */ f(y, {
152
+ /* @__PURE__ */ _(w, {
85
153
  id: "invite-email",
86
154
  type: "email",
87
- value: E,
155
+ value: P,
88
156
  onChange: (e) => {
89
- D(e.target.value), F(null);
157
+ F(e.target.value), U(null), K(null);
90
158
  },
91
- onBlur: () => k(!0),
92
- "aria-invalid": O && !R,
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
- O && E.trim() !== "" && !R ? /* @__PURE__ */ f("p", {
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__ */ f("p", {
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: "They receive an email with a link to accept."
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__ */ p("div", { children: [/* @__PURE__ */ f("label", {
176
+ /* @__PURE__ */ v("div", { children: [/* @__PURE__ */ _("label", {
109
177
  htmlFor: "invite-role",
110
178
  className: "text-sm font-medium",
111
- children: "Access level"
112
- }), C.loading && !C.data ? /* @__PURE__ */ f("p", {
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
- }) : C.error ? /* @__PURE__ */ p("p", {
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: ["Access levels could not be loaded: ", C.error.message]
119
- }) : I.length === 0 ? /* @__PURE__ */ f("p", {
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__ */ p(d, { children: [/* @__PURE__ */ p("select", {
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: A,
126
- onChange: (e) => j(e.target.value),
193
+ value: R,
194
+ onChange: (e) => z(e.target.value),
127
195
  "data-testid": "invite-role",
128
- children: [/* @__PURE__ */ f("option", {
196
+ children: [/* @__PURE__ */ _("option", {
129
197
  value: "",
130
- children: "Choose an access level"
131
- }), I.map((e) => /* @__PURE__ */ f("option", {
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
- }), L?.description && /* @__PURE__ */ f("p", {
203
+ }), Y?.description && /* @__PURE__ */ _("p", {
136
204
  className: "mt-1 text-xs text-muted-foreground",
137
- children: L.description
205
+ children: Y.description
138
206
  })] })] }),
139
- /* @__PURE__ */ f("div", {
207
+ /* @__PURE__ */ _("div", {
140
208
  className: "flex justify-end",
141
- children: /* @__PURE__ */ p(m, {
209
+ children: /* @__PURE__ */ v(y, {
142
210
  size: "sm",
143
- onClick: async () => {
144
- if (!(!z || !L)) {
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__ */ f(a, { className: "mr-1 h-4 w-4" }), T.loading ? "Sending…" : "Send invite"]
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 { S as TeamInviteMemberPagePage };
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":";;;;;;;;;;;;;;AAkCA,IAAM,IAAe,CAAG;;;;;;;;;;;;GAclB,IAAoB,CAAG;;;;;;;;;;;;GA2BvB,IAAgB;CAAE,gBAAgB,CAAC,EAA0B;CAAE,qBAAqB;CAAM;AAEhG,SAAgB,IAA2B;CACzC,IAAM,IAAK,IAAO,EACZ,EAAE,gBAAa,mBAAgB,GAAe,EAC9C,IAAW,IAAuB,EAElC,IAAQ,EAAkC,GAAc;EAC5D,WAAW,EAAE,SAAS,GAAa;EACnC,MAAM,CAAC;EACR,CAAC,EACI,CAAC,IAAQ,KAAe,EAE3B,GAAmB,EAAc,EAC9B,CAAC,GAAQ,KAAe,EAC5B,IACA,EACD,EAEK,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAc,KAAc,EAAmC,KAAK,EACrE,IAAU,GAAc,iBAAiB,KAAe,QAAQ,IAAe,MAC/E,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAE7D,IAAY,QAAc,EAAM,MAAM,SAAS,EAAE,EAAE,CAAC,EAAM,KAAK,CAAC,EAChE,IAAY,QAAc,IAAI,IAAI,EAAU,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,EAAU,CAAC,EACrF,IAAS,EAAU,MAAM,MAAM,EAAE,OAAO,EAAO,IAAI,MACnD,IAAU,EAAmB,EAAM,EACnC,IAAU,KAAW,CAAC,CAAC,KAAU,CAAC,EAAY,WAAW,CAAC,CAAC,GAC3D,IAAY,EAChB,uCACA,2EACD,EAEK,KAAO,YAAY;AACnB,SAAC,KAAW,CAAC,IAGjB;GAFA,EAAS,KAAK,EACd,EAAW,KAAK,EAChB,EAAe,KAAK;AACpB,OAAI;IAIF,IAAM,KAHS,MAAM,EACnB,GAAO,EAAE,WAAW,EAAE,OAAO;KAAE,OAAO,EAAM,MAAM;KAAE,SAAS,CAAC,EAAO,GAAG;KAAE;KAAa,EAAE,EAAE,CAAC,CAC7F,EACyB,MAAM;AAGhC,IAFI,KAAY,EAAW;KAAE,GAAG;KAAY,aAAa,KAAe;KAAM,CAAC,EAC/E,EAAS,GAAG,EACZ,EAAgB,GAAM;YACf,GAAG;AACV,MACE,EACE,GACA,EAAG,mCAAmC,uCAAuC,EAC7E,EACD,CACF;;;IAIC,KAAgB,YAAY;AAC3B,SACL;KAAe,KAAK;AACpB,OAAI;IAEF,IAAM,KADS,MAAM,EAAa,EAAO,EAAE,WAAW,EAAE,IAAI,EAAQ,IAAI,EAAE,CAAC,CAAC,EACrD,MAAM;AAC7B,IAAI,KACF,EAAW;KACT,GAAG;KACH,aAAa,EAAQ;KACrB,oBAAoB,EAAQ;KAC5B,gBAAgB,EAAQ;KACzB,CAAC;YAEG,GAAG;AACV,MACE,EACE,GACA,EAAG,mCAAmC,4CAA4C,EAClF,EACD,CACF;;;IAIC,IAAgB,IAAU,EAAsB,EAAQ,eAAe,GAAG;AAEhF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,2BAA2B,qBAAqB;KACjD,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,8BACA,mEACD;KACC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD;KAAQ,SAAQ;KAAU,MAAK;KAAK,eAAe,EAAS,kBAAkB;eAA9E,CACE,kBAAC,IAAD,EAAG,WAAU,gBAAiB,CAAA,EAC7B,EAAG,0BAA0B,eAAe,CACtC;OACL;;GAEL,KAAW,EAAoB,EAAQ,IACtC,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;IACV,eAAY;cAEX,EACC,0BACA,2EACA,EAAE,OAAO,EAAQ,OAAO,CACzB;IACG,CAAA;GAEP,KAAW,CAAC,EAAoB,EAAQ,IAAI,KAC3C,kBAAC,OAAD;IACE,MAAK;IACL,WAAU;IACV,eAAY;cAHd;KAKE,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,kCACA,iEACA,EAAE,OAAO,EAAQ,OAAO,CACzB;MACC,CAAA;KACJ,kBAAC,KAAD;MAAG,WAAU;MAAwB,eAAY;gBAC9C,EAAG,EAAc,KAAK,EAAc,SAAS;MAC5C,CAAA;KACJ,kBAAC,GAAD;MACE,MAAK;MACL,SAAQ;MACR,eAAe,KAAK,IAAe;MACnC,UAAU,EAAY;MACtB,eAAY;gBALd,CAOE,kBAAC,GAAD,EAAW,WAAU,gBAAiB,CAAA,EACrC,EAAY,UACT,EAAG,+BAA+B,aAAa,GAC/C,EAAG,4BAA4B,eAAe,CAC3C;;KACR,KACC,kBAAC,KAAD;MAAG,MAAK;MAAQ,WAAU;MAAmB,eAAY;gBACtD;MACC,CAAA;KAEF;;GAEP,KACC,kBAAC,KAAD;IAAG,MAAK;IAAQ,WAAU;IAA2B,eAAY;cAC9D;IACC,CAAA;GAGN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;IAAW,WAAU;cAArB,CACE,kBAAC,GAAD,EAAU,WAAU,WAAY,CAAA,EAC/B,EAAG,6BAA6B,qBAAqB,CAC5C;OACD,CAAA,EACb,kBAAC,GAAD;IAAa,WAAU;cAAvB;KACE,kBAAC,OAAD,EAAA,UAAA;MACE,kBAAC,SAAD;OAAO,SAAQ;OAAe,WAAU;iBACrC,EAAG,gCAAgC,gBAAgB;OAC9C,CAAA;MACR,kBAAC,GAAD;OACE,IAAG;OACH,MAAK;OACL,OAAO;OACP,WAAW,MAAM;AAGf,QAFA,EAAS,EAAE,OAAO,MAAM,EACxB,EAAW,KAAK,EAChB,EAAe,KAAK;;OAEtB,cAAc,EAAgB,GAAK;OACnC,gBAAc,KAAgB,CAAC;OAC/B,oBAAiB;OACjB,WAAU;OACV,aAAY;OACZ,CAAA;MACD,KAAgB,EAAM,MAAM,KAAK,MAAM,CAAC,IACvC,kBAAC,KAAD;OAAG,IAAG;OAAoB,MAAK;OAAQ,WAAU;iBAC9C,EACC,kCACA,qDACD;OACC,CAAA,GAEJ,kBAAC,KAAD;OAAG,IAAG;OAAoB,WAAU;iBACjC,EACC,+BACA,4GACD;OACC,CAAA;MAEF,EAAA,CAAA;KAEN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;MAAO,SAAQ;MAAc,WAAU;gBACpC,EAAG,+BAA+B,eAAe;MAC5C,CAAA,EACP,EAAM,WAAW,CAAC,EAAM,OACvB,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,kCAAkC,yBAAyB;MAC7D,CAAA,GACF,EAAM,QACR,kBAAC,KAAD;MAAG,MAAK;MAAQ,WAAU;gBACvB,EACC,iCACA,iDACA,EACE,QAAQ,EAAM,MAAM,SACrB,CACF;MACC,CAAA,GACF,EAAU,WAAW,IACvB,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,+BACA,iEACD;MACC,CAAA,GAEJ,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,UAAD;MACE,IAAG;MACH,WAAU;MACV,OAAO;MACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;MAC1C,eAAY;gBALd,CAOE,kBAAC,UAAD;OAAQ,OAAM;iBACX,EAAG,qCAAqC,yBAAyB;OAC3D,CAAA,EACR,EAAU,KAAK,MACd,kBAAC,UAAD;OAAmB,OAAO,EAAE;iBACzB,EAAE;OACI,EAFI,EAAE,GAEN,CACT,CACK;SACR,GAAQ,eACP,kBAAC,KAAD;MAAG,WAAU;gBAAsC,EAAO;MAAgB,CAAA,CAE3E,EAAA,CAAA,CAED,EAAA,CAAA;KAEN,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAQ,MAAK;OAAK,SAAS;OAAM,UAAU,CAAC;OAAS,eAAY;iBAAjE,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAChC,EAAY,UACT,EAAG,6BAA6B,WAAW,GAC3C,EAAG,0BAA0B,cAAc,CACxC;;MACL,CAAA;KACM;MACT,EAAA,CAAA;GAEP,kBAAC,GAAD;IAEe;IACb,eAAe,GAAa;IACjB;IACX,EAJK,KAAe,eAIpB;GACE"}
@@ -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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.1002.3",
3
+ "version": "2026.1008.1",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [