@burdenoff/microfe-planmagnet 2026.1002.3 → 2026.1008.2

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":"AA8EA,wBAAgB,wBAAwB,4CAwRvC"}
@@ -1,14 +1,19 @@
1
1
  import { usePlanMagnet as e, usePlanMagnetNavigate as t } from "../providers/PlanMagnetProvider.js";
2
- import { throwOnError as n } from "../utils/mutationResult.js";
3
- 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";
2
+ import { useTr as n } from "../../shared/hooks/useTr.js";
3
+ import { throwOnError as r } from "../utils/mutationResult.js";
4
+ import { refusalMessage as i } from "../product/refusal.js";
5
+ import { isValidInviteEmail as a } from "../utils/email.js";
6
+ import { invitationEmailReason as o, invitationEmailSent as s } from "../team/invitationEmail.js";
7
+ import { PENDING_INVITATIONS_QUERY as c, PendingInvitations as l, RESEND_INVITATION as u } from "../team/PendingInvitations.js";
8
+ import { useMemo as d, useState as f } from "react";
9
+ import { RefreshCw as p, Send as ee, UserPlus as te, X as ne } from "lucide-react";
10
+ import { gql as m } from "@apollo/client";
11
+ import { useApolloClient as re, useMutation as h, useQuery as ie } from "@apollo/client/react";
12
+ import { Fragment as ae, jsx as g, jsxs as _ } from "react/jsx-runtime";
13
+ import { Button as v, Card as y, CardContent as b, CardHeader as x, CardTitle as S, Input as C } from "@burdenoff/fe-libs/ui";
14
+ import { clearGraphqlFetchResponseCache as w } from "@burdenoff/fe-libs/shared/graphql";
10
15
  //#region src/planmagnet/pages/TeamInviteMemberPagePage.tsx
11
- var b = c`
16
+ var T = m`
12
17
  query PlanmagnetInviteRoles($scopeId: String!) {
13
18
  roles(
14
19
  scopeId: $scopeId
@@ -20,151 +25,206 @@ var b = c`
20
25
  description
21
26
  }
22
27
  }
23
- `, x = c`
28
+ `, E = m`
24
29
  mutation PlanmagnetInviteMember($input: CreateInvitationInput!) {
25
30
  createInvitation(input: $input) {
26
31
  id
27
32
  email
28
33
  status
29
34
  expiresAt
35
+ emailStatus
36
+ emailLastAttemptAt
37
+ emailErrorCode
30
38
  }
31
39
  }
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", {
40
+ `, D = {
41
+ refetchQueries: [c],
42
+ awaitRefetchQueries: !0
43
+ };
44
+ function O() {
45
+ let m = n(), { workspaceId: O } = e(), oe = t(), k = re(), A = ie(T, {
46
+ variables: { scopeId: O },
47
+ skip: !O
48
+ }), [j, M] = h(E, D), [N, P] = h(u, D), [F, I] = f(""), [L, R] = f(!1), [z, B] = f(""), [V, H] = f(null), [U, W] = f(null), G = U?.workspaceId === (O ?? null) ? U : null, [K, q] = f(null), J = d(() => A.data?.roles ?? [], [A.data]), se = d(() => new Map(J.map((e) => [e.id, e.name])), [J]), Y = J.find((e) => e.id === z) ?? null, X = a(F), Z = X && !!Y && !M.loading && !!O, Q = m("planmagnet.invite.pending.forbidden", "Your access level does not allow managing invitations in this workspace."), ce = async () => {
49
+ if (!(!Z || !Y)) {
50
+ H(null), W(null), q(null);
51
+ try {
52
+ let e = (await r(j({ variables: { input: {
53
+ email: F.trim(),
54
+ roleIds: [Y.id],
55
+ workspaceId: O
56
+ } } }))).data?.createInvitation;
57
+ e && W({
58
+ ...e,
59
+ workspaceId: O ?? null
60
+ }), I(""), R(!1);
61
+ } catch (e) {
62
+ H(i(e, m("planmagnet.invite.errors.create", "The invitation could not be created."), Q));
63
+ }
64
+ }
65
+ }, le = async () => {
66
+ if (G) {
67
+ q(null);
68
+ try {
69
+ let e = (await r(N({ variables: { id: G.id } }))).data?.resendInvitation;
70
+ e && W({
71
+ ...G,
72
+ emailStatus: e.emailStatus,
73
+ emailLastAttemptAt: e.emailLastAttemptAt,
74
+ emailErrorCode: e.emailErrorCode
75
+ });
76
+ } catch (e) {
77
+ q(i(e, m("planmagnet.invite.errors.resend", "The invitation email could not be resent."), Q)), w(), k.refetchQueries({ include: [c] });
78
+ }
79
+ }
80
+ }, $ = G ? o(G.emailErrorCode) : null;
81
+ return /* @__PURE__ */ _("div", {
39
82
  className: "max-w-3xl space-y-6 p-6",
40
83
  children: [
41
- /* @__PURE__ */ p("div", {
84
+ /* @__PURE__ */ _("div", {
42
85
  className: "flex items-center justify-between",
43
- children: [/* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("h1", {
86
+ children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("h1", {
44
87
  className: "text-2xl font-semibold",
45
- children: "Invite Team Member"
46
- }), /* @__PURE__ */ f("p", {
88
+ children: m("planmagnet.invite.title", "Invite Team Member")
89
+ }), /* @__PURE__ */ g("p", {
47
90
  className: "text-sm text-muted-foreground",
48
- children: "Invite someone to this workspace with a PlanMagnet access level."
49
- })] }), /* @__PURE__ */ p(m, {
91
+ children: m("planmagnet.invite.subtitle", "Invite someone to this workspace with a PlanMagnet access level.")
92
+ })] }), /* @__PURE__ */ _(v, {
50
93
  variant: "outline",
51
94
  size: "sm",
52
- onClick: () => S("/team/team-page"),
53
- children: [/* @__PURE__ */ f(s, { className: "mr-1 h-4 w-4" }), "Back to team"]
95
+ onClick: () => oe("/team/team-page"),
96
+ children: [/* @__PURE__ */ g(ne, { className: "mr-1 h-4 w-4" }), m("planmagnet.invite.back", "Back to team")]
54
97
  })]
55
98
  }),
56
- P && /* @__PURE__ */ p("div", {
99
+ G && s(G) && /* @__PURE__ */ g("div", {
57
100
  role: "status",
58
101
  className: "rounded-md border border-border bg-muted p-3 text-sm text-status-success-text",
59
102
  "data-testid": "invite-sent",
103
+ children: m("planmagnet.invite.sent", "Invitation sent to {{email}}. They get this access when they accept it.", { email: G.email })
104
+ }),
105
+ G && !s(G) && $ && /* @__PURE__ */ _("div", {
106
+ role: "status",
107
+ className: "space-y-2 rounded-md border border-border bg-muted p-3 text-sm",
108
+ "data-testid": "invite-email-not-sent",
60
109
  children: [
61
- "Invitation sent to ",
62
- P,
63
- ". They get this access when they accept it."
110
+ /* @__PURE__ */ g("p", {
111
+ className: "font-medium text-status-warning-text",
112
+ children: m("planmagnet.invite.emailNotSent", "Invitation created for {{email}}, but the email was not sent.", { email: G.email })
113
+ }),
114
+ !K && /* @__PURE__ */ g("p", {
115
+ className: "text-muted-foreground",
116
+ "data-testid": "invite-email-not-sent-reason",
117
+ children: m($.key, $.fallback)
118
+ }),
119
+ /* @__PURE__ */ _(v, {
120
+ size: "sm",
121
+ variant: "outline",
122
+ onClick: () => void le(),
123
+ disabled: P.loading,
124
+ "data-testid": "invite-resend",
125
+ children: [/* @__PURE__ */ g(p, { className: "mr-1 h-4 w-4" }), P.loading ? m("planmagnet.invite.resending", "Resending…") : m("planmagnet.invite.resend", "Resend email")]
126
+ }),
127
+ K && /* @__PURE__ */ g("p", {
128
+ role: "alert",
129
+ className: "text-destructive",
130
+ "data-testid": "invite-resend-error",
131
+ children: K
132
+ })
64
133
  ]
65
134
  }),
66
- M && /* @__PURE__ */ f("p", {
135
+ V && /* @__PURE__ */ g("p", {
67
136
  role: "alert",
68
137
  className: "text-sm text-destructive",
69
138
  "data-testid": "invite-error",
70
- children: M
139
+ children: V
71
140
  }),
72
- /* @__PURE__ */ p(h, { children: [/* @__PURE__ */ f(_, { children: /* @__PURE__ */ p(v, {
141
+ /* @__PURE__ */ _(y, { children: [/* @__PURE__ */ g(x, { children: /* @__PURE__ */ _(S, {
73
142
  className: "flex items-center gap-2",
74
- children: [/* @__PURE__ */ f(o, { className: "h-4 w-4" }), "Invitation details"]
75
- }) }), /* @__PURE__ */ p(g, {
143
+ children: [/* @__PURE__ */ g(te, { className: "h-4 w-4" }), m("planmagnet.invite.details", "Invitation details")]
144
+ }) }), /* @__PURE__ */ _(b, {
76
145
  className: "space-y-4",
77
146
  children: [
78
- /* @__PURE__ */ p("div", { children: [
79
- /* @__PURE__ */ f("label", {
147
+ /* @__PURE__ */ _("div", { children: [
148
+ /* @__PURE__ */ g("label", {
80
149
  htmlFor: "invite-email",
81
150
  className: "text-sm font-medium",
82
- children: "Email address"
151
+ children: m("planmagnet.invite.emailLabel", "Email address")
83
152
  }),
84
- /* @__PURE__ */ f(y, {
153
+ /* @__PURE__ */ g(C, {
85
154
  id: "invite-email",
86
155
  type: "email",
87
- value: E,
156
+ value: F,
88
157
  onChange: (e) => {
89
- D(e.target.value), F(null);
158
+ I(e.target.value), W(null), q(null);
90
159
  },
91
- onBlur: () => k(!0),
92
- "aria-invalid": O && !R,
160
+ onBlur: () => R(!0),
161
+ "aria-invalid": L && !X,
93
162
  "aria-describedby": "invite-email-help",
94
163
  className: "mt-1",
95
164
  placeholder: "colleague@company.com"
96
165
  }),
97
- O && E.trim() !== "" && !R ? /* @__PURE__ */ f("p", {
166
+ L && F.trim() !== "" && !X ? /* @__PURE__ */ g("p", {
98
167
  id: "invite-email-help",
99
168
  role: "alert",
100
169
  className: "mt-1 text-xs text-destructive",
101
- children: "Enter a full email address, like name@company.com."
102
- }) : /* @__PURE__ */ f("p", {
170
+ children: m("planmagnet.invite.emailInvalid", "Enter a full email address, like name@company.com.")
171
+ }) : /* @__PURE__ */ g("p", {
103
172
  id: "invite-email-help",
104
173
  className: "mt-1 text-xs text-muted-foreground",
105
- children: "They receive an email with a link to accept."
174
+ children: m("planmagnet.invite.emailHelp", "An invitation email with a link to accept is sent when possible. Pending invitations can be resent below.")
106
175
  })
107
176
  ] }),
108
- /* @__PURE__ */ p("div", { children: [/* @__PURE__ */ f("label", {
177
+ /* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("label", {
109
178
  htmlFor: "invite-role",
110
179
  className: "text-sm font-medium",
111
- children: "Access level"
112
- }), C.loading && !C.data ? /* @__PURE__ */ f("p", {
180
+ children: m("planmagnet.invite.roleLabel", "Access level")
181
+ }), A.loading && !A.data ? /* @__PURE__ */ g("p", {
113
182
  className: "mt-1 text-sm text-muted-foreground",
114
- children: "Loading access levels…"
115
- }) : C.error ? /* @__PURE__ */ p("p", {
183
+ children: m("planmagnet.invite.rolesLoading", "Loading access levels…")
184
+ }) : A.error ? /* @__PURE__ */ g("p", {
116
185
  role: "alert",
117
186
  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", {
187
+ children: m("planmagnet.invite.rolesFailed", "Access levels could not be loaded: {{reason}}", { reason: A.error.message })
188
+ }) : J.length === 0 ? /* @__PURE__ */ g("p", {
120
189
  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", {
190
+ children: m("planmagnet.invite.rolesNone", "No PlanMagnet access levels are set up for this workspace yet.")
191
+ }) : /* @__PURE__ */ _(ae, { children: [/* @__PURE__ */ _("select", {
123
192
  id: "invite-role",
124
193
  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),
194
+ value: z,
195
+ onChange: (e) => B(e.target.value),
127
196
  "data-testid": "invite-role",
128
- children: [/* @__PURE__ */ f("option", {
197
+ children: [/* @__PURE__ */ g("option", {
129
198
  value: "",
130
- children: "Choose an access level"
131
- }), I.map((e) => /* @__PURE__ */ f("option", {
199
+ children: m("planmagnet.invite.rolePlaceholder", "Choose an access level")
200
+ }), J.map((e) => /* @__PURE__ */ g("option", {
132
201
  value: e.id,
133
202
  children: e.name
134
203
  }, e.id))]
135
- }), L?.description && /* @__PURE__ */ f("p", {
204
+ }), Y?.description && /* @__PURE__ */ g("p", {
136
205
  className: "mt-1 text-xs text-muted-foreground",
137
- children: L.description
206
+ children: Y.description
138
207
  })] })] }),
139
- /* @__PURE__ */ f("div", {
208
+ /* @__PURE__ */ g("div", {
140
209
  className: "flex justify-end",
141
- children: /* @__PURE__ */ p(m, {
210
+ children: /* @__PURE__ */ _(v, {
142
211
  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,
212
+ onClick: ce,
213
+ disabled: !Z,
158
214
  "data-testid": "invite-send",
159
- children: [/* @__PURE__ */ f(a, { className: "mr-1 h-4 w-4" }), T.loading ? "Sending…" : "Send invite"]
215
+ children: [/* @__PURE__ */ g(ee, { className: "mr-1 h-4 w-4" }), M.loading ? m("planmagnet.invite.sending", "Sending…") : m("planmagnet.invite.send", "Send invite")]
160
216
  })
161
217
  })
162
218
  ]
163
- })] })
219
+ })] }),
220
+ /* @__PURE__ */ g(l, {
221
+ workspaceId: O,
222
+ roleNames: se
223
+ }, O ?? "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 { useApolloClient, useMutation, useQuery } from '@apollo/client/react';\nimport { clearGraphqlFetchResponseCache } from '@burdenoff/fe-libs/shared/graphql';\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 } = usePlanMagnet();\n const navigate = usePlanMagnetNavigate();\n const client = useApolloClient();\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 // A resend that did not send records its new outcome before it fails, and a failed write runs no\n // refetchQueries: drop fe-libs' 15 s fetch replay and re-read the list, whose row then shows the latest attempt\n // and reason (this block's own reason is hidden while the error stands, as it may be the superseded one).\n clearGraphqlFetchResponseCache();\n void client.refetchQueries({ include: [PENDING_INVITATIONS_QUERY] });\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 {!resendError && (\n <p className=\"text-muted-foreground\" data-testid=\"invite-email-not-sent-reason\">\n {tr(notSentReason.key, notSentReason.fallback)}\n </p>\n )}\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 roleNames={roleNames}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,25 @@
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, 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
+ /** PlanMagnet access-level names by role id, as the page already loaded them. */
23
+ roleNames: ReadonlyMap<string, string>;
24
+ }): import("react/jsx-runtime").JSX.Element;
25
+ //# 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;AAuBxE,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;AAwBD,wBAAgB,kBAAkB,CAAC,EACjC,WAAW,EACX,SAAS,GACV,EAAE;IACD,mHAAmH;IACnH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iFAAiF;IACjF,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC,2CAyRA"}
@@ -0,0 +1,219 @@
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
+ expiresAt
24
+ emailStatus
25
+ emailLastAttemptAt
26
+ emailErrorCode
27
+ }
28
+ total
29
+ hasMore
30
+ }
31
+ }
32
+ `, A = h`
33
+ mutation PlanmagnetResendInvitation($id: ID!) {
34
+ resendInvitation(id: $id) {
35
+ id
36
+ status
37
+ expiresAt
38
+ emailStatus
39
+ emailLastAttemptAt
40
+ emailErrorCode
41
+ }
42
+ }
43
+ `, j = h`
44
+ mutation PlanmagnetCancelInvitation($id: ID!) {
45
+ cancelInvitation(id: $id) {
46
+ id
47
+ status
48
+ }
49
+ }
50
+ `, M = {
51
+ refetchQueries: [D],
52
+ awaitRefetchQueries: !0
53
+ }, N = {
54
+ success: "text-status-success-text",
55
+ warning: "text-status-warning-text",
56
+ error: "text-destructive"
57
+ }, P = (e) => u(e)?.toLocaleString() ?? null;
58
+ function F({ workspaceId: h, roleNames: D }) {
59
+ let F = e(), { data: I, loading: L, error: R, refetch: z } = _(k, {
60
+ variables: { pagination: { limit: O } },
61
+ skip: !h,
62
+ fetchPolicy: "network-only"
63
+ }), [B] = g(A, M), [V] = g(j, M), [H, U] = d(null), [W, G] = d(null), [K] = d(() => Date.now()), q = F("planmagnet.invite.pending.forbidden", "Your access level does not allow managing invitations in this workspace."), J = () => {
64
+ E(), z();
65
+ }, Y = async (e) => {
66
+ U(e.id), G(null);
67
+ try {
68
+ let t = (await n(B({ variables: { id: e.id } }))).data?.resendInvitation;
69
+ if (s(t)) G({
70
+ tone: "success",
71
+ text: F("planmagnet.invite.pending.resent", "Invitation email sent again to {{email}}.", { email: e.email })
72
+ });
73
+ else {
74
+ let n = o(t?.emailErrorCode);
75
+ G({
76
+ tone: "warning",
77
+ text: F("planmagnet.invite.pending.resendNotSent", "The invitation to {{email}} is still pending, but the email was not sent. {{reason}}", {
78
+ email: e.email,
79
+ reason: F(n.key, n.fallback)
80
+ })
81
+ });
82
+ }
83
+ } catch (t) {
84
+ G({
85
+ tone: "error",
86
+ text: F("planmagnet.invite.pending.resendFailed", "The invitation to {{email}} could not be resent: {{reason}}", {
87
+ email: e.email,
88
+ reason: i(t, F("planmagnet.invite.pending.resendFailedFallback", "Try again."), q)
89
+ })
90
+ }), J();
91
+ } finally {
92
+ U(null);
93
+ }
94
+ }, X = async (e) => {
95
+ U(e.id), G(null);
96
+ try {
97
+ await n(V({ variables: { id: e.id } })), G({
98
+ tone: "success",
99
+ text: F("planmagnet.invite.pending.cancelled", "The invitation to {{email}} was cancelled.", { email: e.email })
100
+ });
101
+ } catch (t) {
102
+ G({
103
+ tone: "error",
104
+ text: F("planmagnet.invite.pending.cancelFailed", "The invitation to {{email}} could not be cancelled: {{reason}}", {
105
+ email: e.email,
106
+ reason: i(t, F("planmagnet.invite.pending.cancelFailedFallback", "Try again."), q)
107
+ })
108
+ }), J();
109
+ } finally {
110
+ U(null);
111
+ }
112
+ }, Z = I?.workspaceInvitations, Q = Z?.items ?? [], $ = (e) => {
113
+ let { names: t, unknown: n } = l(e, D), r = [...t];
114
+ return n > 0 && r.push(F("planmagnet.invite.pending.otherRoles", "{{count}} other access level(s)", { count: n })), r.length > 0 ? r.join(", ") : F("planmagnet.invite.pending.noRoles", "No access level");
115
+ };
116
+ return /* @__PURE__ */ y(S, {
117
+ "data-testid": "pending-invitations",
118
+ children: [/* @__PURE__ */ v(w, { children: /* @__PURE__ */ y(T, {
119
+ className: "flex items-center gap-2",
120
+ children: [/* @__PURE__ */ v(f, { className: "h-4 w-4" }), F("planmagnet.invite.pending.title", "Pending invitations")]
121
+ }) }), /* @__PURE__ */ y(C, {
122
+ className: "space-y-3",
123
+ children: [
124
+ W && /* @__PURE__ */ v("p", {
125
+ role: W.tone === "error" ? "alert" : "status",
126
+ className: `text-sm ${N[W.tone]}`,
127
+ "data-testid": "pending-invitations-notice",
128
+ children: W.text
129
+ }),
130
+ L && !I ? /* @__PURE__ */ v("p", {
131
+ className: "text-sm text-muted-foreground",
132
+ children: F("planmagnet.invite.pending.loading", "Loading pending invitations…")
133
+ }) : R && !I ? /* @__PURE__ */ v("p", {
134
+ role: "alert",
135
+ className: "text-sm text-destructive",
136
+ children: t(R) ? q : F("planmagnet.invite.pending.loadFailed", "Pending invitations could not be loaded: {{reason}}", { reason: R.message })
137
+ }) : Q.length === 0 ? /* @__PURE__ */ v("p", {
138
+ className: "text-sm text-muted-foreground",
139
+ children: F("planmagnet.invite.pending.empty", "No invitations are waiting to be accepted.")
140
+ }) : /* @__PURE__ */ v("ul", {
141
+ className: "divide-y divide-border",
142
+ "data-testid": "pending-invitations-list",
143
+ children: Q.map((e) => {
144
+ let t = s(e), n = c(e.emailStatus), i = o(e.emailErrorCode), l = P(e.emailLastAttemptAt), d = u(e.expiresAt), f = !!d && d.getTime() <= K, h = H === e.id;
145
+ return /* @__PURE__ */ y("li", {
146
+ className: "flex flex-col gap-2 py-3 sm:flex-row sm:items-start sm:justify-between",
147
+ "data-testid": "pending-invitation",
148
+ children: [/* @__PURE__ */ y("div", {
149
+ className: "min-w-0 space-y-1",
150
+ children: [
151
+ /* @__PURE__ */ v("p", {
152
+ className: "truncate text-sm font-medium",
153
+ children: e.email
154
+ }),
155
+ /* @__PURE__ */ v("p", {
156
+ className: "text-xs text-muted-foreground",
157
+ children: $(e.roleIds)
158
+ }),
159
+ /* @__PURE__ */ y("div", {
160
+ className: "flex flex-wrap items-center gap-2 text-xs text-muted-foreground",
161
+ children: [
162
+ /* @__PURE__ */ v(b, {
163
+ variant: a(e.emailStatus),
164
+ "data-testid": "pending-invitation-email-status",
165
+ children: F(n.key, n.fallback)
166
+ }),
167
+ l && /* @__PURE__ */ v("span", { children: F("planmagnet.invite.pending.lastAttempt", "Last tried {{when}}", { when: l }) }),
168
+ d && /* @__PURE__ */ v("span", {
169
+ className: f ? "text-status-warning-text" : void 0,
170
+ children: f ? F("planmagnet.invite.pending.expired", "Expired {{when}}", { when: d.toLocaleString() }) : F("planmagnet.invite.pending.expires", "Expires {{when}}", { when: d.toLocaleString() })
171
+ })
172
+ ]
173
+ }),
174
+ !t && e.emailStatus && /* @__PURE__ */ v("p", {
175
+ className: "text-xs text-status-warning-text",
176
+ children: F(i.key, i.fallback)
177
+ })
178
+ ]
179
+ }), /* @__PURE__ */ y("div", {
180
+ className: "flex shrink-0 gap-2",
181
+ children: [/* @__PURE__ */ y(x, {
182
+ size: "sm",
183
+ variant: "outline",
184
+ disabled: H !== null,
185
+ onClick: () => void Y(e),
186
+ "data-testid": "pending-invitation-resend",
187
+ children: [/* @__PURE__ */ v(p, { className: "mr-1 h-4 w-4" }), h ? F("planmagnet.invite.pending.working", "Working…") : F("planmagnet.invite.pending.resend", "Resend")]
188
+ }), /* @__PURE__ */ v(r, {
189
+ label: F("planmagnet.invite.pending.cancel", "Cancel"),
190
+ variant: "outline",
191
+ icon: /* @__PURE__ */ v(m, { className: "mr-1 h-4 w-4" }),
192
+ testId: "pending-invitation-cancel",
193
+ title: F("planmagnet.invite.pending.confirmCancel.title", "Cancel the invitation to {{email}}?", { email: e.email }),
194
+ description: F("planmagnet.invite.pending.confirmCancel.description", "Its link stops working and they will not join this workspace from it. You can invite them again later."),
195
+ confirmLabel: F("planmagnet.invite.pending.confirmCancel.confirm", "Cancel invitation"),
196
+ cancelLabel: F("planmagnet.invite.pending.confirmCancel.keep", "Keep it"),
197
+ destructive: !0,
198
+ disabled: H !== null,
199
+ onConfirm: () => void X(e)
200
+ })]
201
+ })]
202
+ }, e.id);
203
+ })
204
+ }),
205
+ Z?.hasMore && /* @__PURE__ */ v("p", {
206
+ className: "text-xs text-muted-foreground",
207
+ children: F("planmagnet.invite.pending.truncated", "Showing the first {{shown}} of {{total}} pending invitations.", {
208
+ shown: Q.length,
209
+ total: Z.total
210
+ })
211
+ })
212
+ ]
213
+ })]
214
+ });
215
+ }
216
+ //#endregion
217
+ export { D as PENDING_INVITATIONS_QUERY, F as PendingInvitations, A as RESEND_INVITATION };
218
+
219
+ //# 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 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}\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 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 /** 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 {/* Every row offers Cancel, as it does Resend: cancelInvitation is an RBAC write\n (invitation:cancel), not an inviter-only one, so the gateway decides and a refusal says why. */}\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 </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.2",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [