@burdenoff/microfe-organization 2026.527.3 → 2026.527.5

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.
Files changed (20) hide show
  1. package/dist/generated/global-types.js +3 -1
  2. package/dist/generated/global-types.js.map +1 -1
  3. package/dist/organization-management/UserOrganizationRoot.js +39 -28
  4. package/dist/organization-management/UserOrganizationRoot.js.map +1 -1
  5. package/dist/organization-management/UserOrganizationRoutes.js +12 -8
  6. package/dist/organization-management/UserOrganizationRoutes.js.map +1 -1
  7. package/dist/organization-management/modules/members/components/BulkInviteForm.js +291 -0
  8. package/dist/organization-management/modules/members/components/BulkInviteForm.js.map +1 -0
  9. package/dist/organization-management/modules/members/pages/InviteMemberPage.js +65 -42
  10. package/dist/organization-management/modules/members/pages/InviteMemberPage.js.map +1 -1
  11. package/dist/organization-management/modules/members/pages/InvitePeoplePage.js +95 -0
  12. package/dist/organization-management/modules/members/pages/InvitePeoplePage.js.map +1 -0
  13. package/dist/organization-management/modules/members/utils/bulkInviteCsv.js +52 -0
  14. package/dist/organization-management/modules/members/utils/bulkInviteCsv.js.map +1 -0
  15. package/dist/organization-management/modules/organizations/pages/OrganizationDetailPage.js +163 -151
  16. package/dist/organization-management/modules/organizations/pages/OrganizationDetailPage.js.map +1 -1
  17. package/dist/organization-management/modules/workspaces/components/AddWorkspaceMemberDialog.js +16 -0
  18. package/dist/organization-management/modules/workspaces/components/AddWorkspaceMemberDialog.js.map +1 -1
  19. package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js +7 -7
  20. package/package.json +1 -1
@@ -5,89 +5,112 @@ import { useTr as r } from "../../../shared/hooks/useTr.js";
5
5
  import { LoadingState as i } from "../../../shared/components/feedback/LoadingState.js";
6
6
  import { PERMISSIONS as a } from "../../../constants/permissions.js";
7
7
  import { InviteMemberForm as o } from "../components/InviteMemberForm.js";
8
- import { PendingInvitesList as s } from "../components/PendingInvitesList.js";
9
- import { ArrowLeft as c, UserPlus as l } from "lucide-react";
10
- import { usePermissions as u } from "@burdenoff/fe-libs/shared/providers/shell";
11
- import { jsx as d, jsxs as f } from "react/jsx-runtime";
12
- import { useParams as p } from "react-router-dom";
8
+ import { BulkInviteForm as s } from "../components/BulkInviteForm.js";
9
+ import { PendingInvitesList as c } from "../components/PendingInvitesList.js";
10
+ import { useState as l } from "react";
11
+ import { ArrowLeft as u, UserPlus as d } from "lucide-react";
12
+ import { usePermissions as f } from "@burdenoff/fe-libs/shared/providers/shell";
13
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
14
+ import { useParams as h } from "react-router-dom";
13
15
  //#region src/organization-management/modules/members/pages/InviteMemberPage.tsx
14
- function m() {
15
- let m = r(), { organizationId: h } = p(), { navigate: g, basePath: _ = "/organizations" } = e(), { hasPermission: v, isLoading: y } = u(), { inviteMember: b, loading: x, error: S } = n(), C = async (e) => {
16
- await b(e), t.success(`${m("members.invite.success", "Invitation sent to")} ${e.email}`);
17
- }, w = () => {
18
- g(`${_}/${h}/members`);
16
+ function g() {
17
+ let g = r(), { organizationId: _ } = h(), { navigate: v, basePath: y = "/organizations" } = e(), { hasPermission: b, isLoading: x } = f(), { inviteMember: S, loading: C, error: w } = n(), [T, E] = l("single"), D = async (e) => {
18
+ await S(e), t.success(`${g("members.invite.success", "Invitation sent to")} ${e.email}`);
19
+ }, O = () => {
20
+ v(`${y}/${_}/members`);
19
21
  };
20
- return y ? /* @__PURE__ */ d("div", {
22
+ return x ? /* @__PURE__ */ p("div", {
21
23
  className: "min-h-full bg-[var(--color-bg-canvas)] p-6",
22
- children: /* @__PURE__ */ d(i, { message: m("common.loadingPermissions", "Loading permissions...") })
23
- }) : v(a.MEMBER_INVITE) ? /* @__PURE__ */ d("div", {
24
+ children: /* @__PURE__ */ p(i, { message: g("common.loadingPermissions", "Loading permissions...") })
25
+ }) : b(a.MEMBER_INVITE) ? /* @__PURE__ */ p("div", {
24
26
  className: "min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6",
25
- children: /* @__PURE__ */ f("div", {
27
+ children: /* @__PURE__ */ m("div", {
26
28
  className: "mx-auto max-w-3xl space-y-8",
27
29
  children: [
28
- /* @__PURE__ */ f("div", {
30
+ /* @__PURE__ */ m("div", {
29
31
  className: "flex items-center gap-3",
30
- children: [/* @__PURE__ */ d("button", {
32
+ children: [/* @__PURE__ */ p("button", {
31
33
  type: "button",
32
- onClick: w,
34
+ onClick: O,
33
35
  className: "rounded-lg p-2 transition-colors hover:bg-[var(--color-bg-subtle)]",
34
- "aria-label": m("members.invite.backToMembers", "Back to members"),
35
- children: /* @__PURE__ */ d(c, {
36
+ "aria-label": g("members.invite.backToMembers", "Back to members"),
37
+ children: /* @__PURE__ */ p(u, {
36
38
  size: 20,
37
39
  className: "text-[var(--color-text-secondary)]"
38
40
  })
39
- }), /* @__PURE__ */ f("div", {
41
+ }), /* @__PURE__ */ m("div", {
40
42
  className: "flex items-center gap-2",
41
- children: [/* @__PURE__ */ d(l, {
43
+ children: [/* @__PURE__ */ p(d, {
42
44
  size: 24,
43
45
  className: "text-[var(--color-text-primary)]"
44
- }), /* @__PURE__ */ d("h1", {
46
+ }), /* @__PURE__ */ p("h1", {
45
47
  className: "text-2xl font-bold text-[var(--color-text-primary)]",
46
- children: m("members.invite.title", "Invite Member")
48
+ children: g("members.invite.title", "Invite Member")
47
49
  })]
48
50
  })]
49
51
  }),
50
- /* @__PURE__ */ f("div", {
52
+ /* @__PURE__ */ p("div", {
53
+ className: "inline-flex rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-1",
54
+ role: "tablist",
55
+ "aria-label": g("members.invite.modeToggle", "Invite mode"),
56
+ children: ["single", "bulk"].map((e) => {
57
+ let t = T === e, n = e === "single" ? g("members.invite.modeSingle", "Single") : g("members.invite.modeBulk", "Bulk (CSV)");
58
+ return /* @__PURE__ */ p("button", {
59
+ type: "button",
60
+ role: "tab",
61
+ "aria-selected": t,
62
+ onClick: () => E(e),
63
+ className: `min-h-9 rounded-md px-4 py-1.5 text-sm font-medium transition-colors ${t ? "bg-[var(--color-action-primary-bg)] text-[var(--color-action-primary-text)]" : "text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]"}`,
64
+ children: n
65
+ }, e);
66
+ })
67
+ }),
68
+ /* @__PURE__ */ m("div", {
51
69
  className: "bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)] p-6",
52
70
  children: [
53
- /* @__PURE__ */ d("h2", {
71
+ /* @__PURE__ */ p("h2", {
54
72
  className: "text-lg font-semibold text-[var(--color-text-primary)] mb-4",
55
- children: m("members.invite.send", "Send Invite")
73
+ children: T === "single" ? g("members.invite.send", "Send Invite") : g("members.bulkInvite.title", "Bulk Invite from CSV")
56
74
  }),
57
- /* @__PURE__ */ d("p", {
75
+ /* @__PURE__ */ p("p", {
58
76
  className: "text-sm text-[var(--color-text-muted)] mb-6",
59
- children: m("members.invite.description", "Enter the email address of the person you want to invite. They will receive an email notification and can accept or decline from their account.")
77
+ children: T === "single" ? g("members.invite.description", "Enter the email address of the person you want to invite. They will receive an email notification and can accept or decline from their account.") : g("members.bulkInvite.pageDescription", "Upload a CSV to invite many people at once. Download the template below to see the expected format.")
60
78
  }),
61
- S && /* @__PURE__ */ d("div", {
79
+ T === "single" && w && /* @__PURE__ */ p("div", {
62
80
  className: "mb-4 rounded-lg border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bgSubtle)] p-3",
63
- children: /* @__PURE__ */ d("p", {
81
+ children: /* @__PURE__ */ p("p", {
64
82
  className: "text-sm text-[var(--color-status-error-text)]",
65
- children: S.message.includes("CONFLICT") ? m("members.invite.error.conflict", "An invite already exists for this email address.") : S.message.includes("already a member") ? m("members.invite.error.alreadyMember", "This user is already a member of the organization.") : S.message
83
+ children: w.message.includes("CONFLICT") ? g("members.invite.error.conflict", "An invite already exists for this email address.") : w.message.includes("already a member") ? g("members.invite.error.alreadyMember", "This user is already a member of the organization.") : w.message
66
84
  })
67
85
  }),
68
- /* @__PURE__ */ d(o, {
69
- organizationId: h,
70
- onSubmit: C,
71
- onCancel: w,
72
- loading: x
86
+ T === "single" ? /* @__PURE__ */ p(o, {
87
+ organizationId: _,
88
+ onSubmit: D,
89
+ onCancel: O,
90
+ loading: C
91
+ }) : /* @__PURE__ */ p(s, {
92
+ organizationId: _,
93
+ inviteMember: S,
94
+ onCancel: O,
95
+ loading: C
73
96
  })
74
97
  ]
75
98
  }),
76
- /* @__PURE__ */ d("div", {
99
+ /* @__PURE__ */ p("div", {
77
100
  className: "bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)] p-6",
78
- children: /* @__PURE__ */ d(s, { organizationId: h })
101
+ children: /* @__PURE__ */ p(c, { organizationId: _ })
79
102
  })
80
103
  ]
81
104
  })
82
- }) : /* @__PURE__ */ d("div", {
105
+ }) : /* @__PURE__ */ p("div", {
83
106
  className: "min-h-full bg-[var(--color-bg-canvas)] p-6",
84
- children: /* @__PURE__ */ d("div", {
107
+ children: /* @__PURE__ */ p("div", {
85
108
  className: "p-8 text-center text-[var(--color-text-secondary)]",
86
- children: m("members.invite.noPermission", "You don't have permission to invite members.")
109
+ children: g("members.invite.noPermission", "You don't have permission to invite members.")
87
110
  })
88
111
  });
89
112
  }
90
113
  //#endregion
91
- export { m as InviteMemberPage };
114
+ export { g as InviteMemberPage };
92
115
 
93
116
  //# sourceMappingURL=InviteMemberPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"InviteMemberPage.js","names":[],"sources":["../../../../../src/organization-management/modules/members/pages/InviteMemberPage.tsx"],"sourcesContent":["import { useParams } from 'react-router-dom';\nimport { ArrowLeft, UserPlus } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useOrgConsole } from '../../../providers/OrgConsoleProvider';\nimport { useInviteMember } from '../hooks/useInvites';\nimport { InviteMemberForm } from '../components/InviteMemberForm';\nimport { PendingInvitesList } from '../components/PendingInvitesList';\nimport { LoadingState } from '../../../shared/components/feedback/LoadingState';\nimport { PERMISSIONS } from '../../../constants/permissions';\nimport { useTr } from '../../../shared/hooks/useTr';\n\n/**\n * Invite Member Page\n * Email-based invite form with pending invites list below\n */\nexport function InviteMemberPage() {\n const tr = useTr();\n const { organizationId } = useParams<{ organizationId: string }>();\n const { navigate, basePath = '/organizations' } = useOrgConsole();\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const { inviteMember, loading, error } = useInviteMember();\n\n const handleSubmit = async (data: {\n organizationId: string;\n email: string;\n message?: string;\n }) => {\n await inviteMember(data);\n toast.success(`${tr('members.invite.success', 'Invitation sent to')} ${data.email}`);\n };\n\n const handleCancel = () => {\n navigate(`${basePath}/${organizationId}/members`);\n };\n\n if (permissionsLoading) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <LoadingState message={tr('common.loadingPermissions', 'Loading permissions...')} />\n </div>\n );\n }\n\n if (!hasPermission(PERMISSIONS.MEMBER_INVITE)) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <div className=\"p-8 text-center text-[var(--color-text-secondary)]\">\n {tr('members.invite.noPermission', \"You don't have permission to invite members.\")}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6\">\n <div className=\"mx-auto max-w-3xl space-y-8\">\n {/* Header */}\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"rounded-lg p-2 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n aria-label={tr('members.invite.backToMembers', 'Back to members')}\n >\n <ArrowLeft size={20} className=\"text-[var(--color-text-secondary)]\" />\n </button>\n <div className=\"flex items-center gap-2\">\n <UserPlus size={24} className=\"text-[var(--color-text-primary)]\" />\n <h1 className=\"text-2xl font-bold text-[var(--color-text-primary)]\">\n {tr('members.invite.title', 'Invite Member')}\n </h1>\n </div>\n </div>\n\n {/* Invite Form Card */}\n <div className=\"bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)] p-6\">\n <h2 className=\"text-lg font-semibold text-[var(--color-text-primary)] mb-4\">\n {tr('members.invite.send', 'Send Invite')}\n </h2>\n <p className=\"text-sm text-[var(--color-text-muted)] mb-6\">\n {tr(\n 'members.invite.description',\n 'Enter the email address of the person you want to invite. They will receive an email notification and can accept or decline from their account.'\n )}\n </p>\n\n {error && (\n <div className=\"mb-4 rounded-lg border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bgSubtle)] p-3\">\n <p className=\"text-sm text-[var(--color-status-error-text)]\">\n {error.message.includes('CONFLICT')\n ? tr(\n 'members.invite.error.conflict',\n 'An invite already exists for this email address.'\n )\n : error.message.includes('already a member')\n ? tr(\n 'members.invite.error.alreadyMember',\n 'This user is already a member of the organization.'\n )\n : error.message}\n </p>\n </div>\n )}\n\n <InviteMemberForm\n organizationId={organizationId!}\n onSubmit={handleSubmit}\n onCancel={handleCancel}\n loading={loading}\n />\n </div>\n\n {/* Pending Invites */}\n <div className=\"bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)] p-6\">\n <PendingInvitesList organizationId={organizationId!} />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,SAAgB,IAAmB;CACjC,IAAM,IAAK,GAAO,EACZ,EAAE,sBAAmB,GAAuC,EAC5D,EAAE,aAAU,cAAW,qBAAqB,GAAe,EAC3D,EAAE,kBAAe,WAAW,MAAuB,GAAgB,EACnE,EAAE,iBAAc,YAAS,aAAU,GAAiB,EAEpD,IAAe,OAAO,MAItB;AAEJ,EADA,MAAM,EAAa,EAAK,EACxB,EAAM,QAAQ,GAAG,EAAG,0BAA0B,qBAAqB,CAAC,GAAG,EAAK,QAAQ;IAGhF,UAAqB;AACzB,IAAS,GAAG,EAAS,GAAG,EAAe,UAAU;;AAqBnD,QAlBI,IAEA,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAc,SAAS,EAAG,6BAA6B,yBAAyB,EAAI,CAAA;EAChF,CAAA,GAIL,EAAc,EAAY,cAAc,GAW3C,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAU;MACV,cAAY,EAAG,gCAAgC,kBAAkB;gBAEjE,kBAAC,GAAD;OAAW,MAAM;OAAI,WAAU;OAAuC,CAAA;MAC/D,CAAA,EACT,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OAAU,MAAM;OAAI,WAAU;OAAqC,CAAA,EACnE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,wBAAwB,gBAAgB;OACzC,CAAA,CACD;QACF;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,uBAAuB,cAAc;OACtC,CAAA;MACL,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,8BACA,kJACD;OACC,CAAA;MAEH,KACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAM,QAAQ,SAAS,WAAW,GAC/B,EACE,iCACA,mDACD,GACD,EAAM,QAAQ,SAAS,mBAAmB,GACxC,EACE,sCACA,qDACD,GACD,EAAM;QACV,CAAA;OACA,CAAA;MAGR,kBAAC,GAAD;OACkB;OAChB,UAAU;OACV,UAAU;OACD;OACT,CAAA;MACE;;IAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAoC,mBAAmB,CAAA;KACnD,CAAA;IACF;;EACF,CAAA,GAxEJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAG,+BAA+B,+CAA+C;GAC9E,CAAA;EACF,CAAA"}
1
+ {"version":3,"file":"InviteMemberPage.js","names":[],"sources":["../../../../../src/organization-management/modules/members/pages/InviteMemberPage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, UserPlus } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useOrgConsole } from '../../../providers/OrgConsoleProvider';\nimport { useInviteMember } from '../hooks/useInvites';\nimport { InviteMemberForm } from '../components/InviteMemberForm';\nimport { BulkInviteForm } from '../components/BulkInviteForm';\nimport { PendingInvitesList } from '../components/PendingInvitesList';\nimport { LoadingState } from '../../../shared/components/feedback/LoadingState';\nimport { PERMISSIONS } from '../../../constants/permissions';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ntype InviteMode = 'single' | 'bulk';\n\n/**\n * Invite Member Page\n * Email-based invite form with pending invites list below\n */\nexport function InviteMemberPage() {\n const tr = useTr();\n const { organizationId } = useParams<{ organizationId: string }>();\n const { navigate, basePath = '/organizations' } = useOrgConsole();\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const { inviteMember, loading, error } = useInviteMember();\n const [mode, setMode] = useState<InviteMode>('single');\n\n const handleSubmit = async (data: {\n organizationId: string;\n email: string;\n message?: string;\n }) => {\n await inviteMember(data);\n toast.success(`${tr('members.invite.success', 'Invitation sent to')} ${data.email}`);\n };\n\n const handleCancel = () => {\n navigate(`${basePath}/${organizationId}/members`);\n };\n\n if (permissionsLoading) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <LoadingState message={tr('common.loadingPermissions', 'Loading permissions...')} />\n </div>\n );\n }\n\n if (!hasPermission(PERMISSIONS.MEMBER_INVITE)) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <div className=\"p-8 text-center text-[var(--color-text-secondary)]\">\n {tr('members.invite.noPermission', \"You don't have permission to invite members.\")}\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6\">\n <div className=\"mx-auto max-w-3xl space-y-8\">\n {/* Header */}\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={handleCancel}\n className=\"rounded-lg p-2 transition-colors hover:bg-[var(--color-bg-subtle)]\"\n aria-label={tr('members.invite.backToMembers', 'Back to members')}\n >\n <ArrowLeft size={20} className=\"text-[var(--color-text-secondary)]\" />\n </button>\n <div className=\"flex items-center gap-2\">\n <UserPlus size={24} className=\"text-[var(--color-text-primary)]\" />\n <h1 className=\"text-2xl font-bold text-[var(--color-text-primary)]\">\n {tr('members.invite.title', 'Invite Member')}\n </h1>\n </div>\n </div>\n\n {/* Mode toggle */}\n <div\n className=\"inline-flex rounded-lg border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-1\"\n role=\"tablist\"\n aria-label={tr('members.invite.modeToggle', 'Invite mode')}\n >\n {(['single', 'bulk'] as const).map((m) => {\n const isActive = mode === m;\n const label =\n m === 'single'\n ? tr('members.invite.modeSingle', 'Single')\n : tr('members.invite.modeBulk', 'Bulk (CSV)');\n return (\n <button\n key={m}\n type=\"button\"\n role=\"tab\"\n aria-selected={isActive}\n onClick={() => setMode(m)}\n className={`min-h-9 rounded-md px-4 py-1.5 text-sm font-medium transition-colors ${\n isActive\n ? 'bg-[var(--color-action-primary-bg)] text-[var(--color-action-primary-text)]'\n : 'text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)]'\n }`}\n >\n {label}\n </button>\n );\n })}\n </div>\n\n {/* Invite Form Card */}\n <div className=\"bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)] p-6\">\n <h2 className=\"text-lg font-semibold text-[var(--color-text-primary)] mb-4\">\n {mode === 'single'\n ? tr('members.invite.send', 'Send Invite')\n : tr('members.bulkInvite.title', 'Bulk Invite from CSV')}\n </h2>\n <p className=\"text-sm text-[var(--color-text-muted)] mb-6\">\n {mode === 'single'\n ? tr(\n 'members.invite.description',\n 'Enter the email address of the person you want to invite. They will receive an email notification and can accept or decline from their account.'\n )\n : tr(\n 'members.bulkInvite.pageDescription',\n 'Upload a CSV to invite many people at once. Download the template below to see the expected format.'\n )}\n </p>\n\n {mode === 'single' && error && (\n <div className=\"mb-4 rounded-lg border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bgSubtle)] p-3\">\n <p className=\"text-sm text-[var(--color-status-error-text)]\">\n {error.message.includes('CONFLICT')\n ? tr(\n 'members.invite.error.conflict',\n 'An invite already exists for this email address.'\n )\n : error.message.includes('already a member')\n ? tr(\n 'members.invite.error.alreadyMember',\n 'This user is already a member of the organization.'\n )\n : error.message}\n </p>\n </div>\n )}\n\n {mode === 'single' ? (\n <InviteMemberForm\n organizationId={organizationId!}\n onSubmit={handleSubmit}\n onCancel={handleCancel}\n loading={loading}\n />\n ) : (\n <BulkInviteForm\n organizationId={organizationId!}\n inviteMember={inviteMember}\n onCancel={handleCancel}\n loading={loading}\n />\n )}\n </div>\n\n {/* Pending Invites */}\n <div className=\"bg-[var(--color-bg-surface)] rounded-xl border border-[var(--color-border-default)] p-6\">\n <PendingInvitesList organizationId={organizationId!} />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAoBA,SAAgB,IAAmB;CACjC,IAAM,IAAK,GAAO,EACZ,EAAE,sBAAmB,GAAuC,EAC5D,EAAE,aAAU,cAAW,qBAAqB,GAAe,EAC3D,EAAE,kBAAe,WAAW,MAAuB,GAAgB,EACnE,EAAE,iBAAc,YAAS,aAAU,GAAiB,EACpD,CAAC,GAAM,KAAW,EAAqB,SAAS,EAEhD,IAAe,OAAO,MAItB;AAEJ,EADA,MAAM,EAAa,EAAK,EACxB,EAAM,QAAQ,GAAG,EAAG,0BAA0B,qBAAqB,CAAC,GAAG,EAAK,QAAQ;IAGhF,UAAqB;AACzB,IAAS,GAAG,EAAS,GAAG,EAAe,UAAU;;AAqBnD,QAlBI,IAEA,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAc,SAAS,EAAG,6BAA6B,yBAAyB,EAAI,CAAA;EAChF,CAAA,GAIL,EAAc,EAAY,cAAc,GAW3C,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,SAAS;MACT,WAAU;MACV,cAAY,EAAG,gCAAgC,kBAAkB;gBAEjE,kBAAC,GAAD;OAAW,MAAM;OAAI,WAAU;OAAuC,CAAA;MAC/D,CAAA,EACT,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OAAU,MAAM;OAAI,WAAU;OAAqC,CAAA,EACnE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,wBAAwB,gBAAgB;OACzC,CAAA,CACD;QACF;;IAGN,kBAAC,OAAD;KACE,WAAU;KACV,MAAK;KACL,cAAY,EAAG,6BAA6B,cAAc;eAExD,CAAC,UAAU,OAAO,CAAW,KAAK,MAAM;MACxC,IAAM,IAAW,MAAS,GACpB,IACJ,MAAM,WACF,EAAG,6BAA6B,SAAS,GACzC,EAAG,2BAA2B,aAAa;AACjD,aACE,kBAAC,UAAD;OAEE,MAAK;OACL,MAAK;OACL,iBAAe;OACf,eAAe,EAAQ,EAAE;OACzB,WAAW,wEACT,IACI,gFACA;iBAGL;OACM,EAZF,EAYE;OAEX;KACE,CAAA;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,MAAS,WACN,EAAG,uBAAuB,cAAc,GACxC,EAAG,4BAA4B,uBAAuB;OACvD,CAAA;MACL,kBAAC,KAAD;OAAG,WAAU;iBACV,MAAS,WACN,EACE,8BACA,kJACD,GACD,EACE,sCACA,sGACD;OACH,CAAA;MAEH,MAAS,YAAY,KACpB,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAM,QAAQ,SAAS,WAAW,GAC/B,EACE,iCACA,mDACD,GACD,EAAM,QAAQ,SAAS,mBAAmB,GACxC,EACE,sCACA,qDACD,GACD,EAAM;QACV,CAAA;OACA,CAAA;MAGP,MAAS,WACR,kBAAC,GAAD;OACkB;OAChB,UAAU;OACV,UAAU;OACD;OACT,CAAA,GAEF,kBAAC,GAAD;OACkB;OACF;OACd,UAAU;OACD;OACT,CAAA;MAEA;;IAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAoC,mBAAmB,CAAA;KACnD,CAAA;IACF;;EACF,CAAA,GAvHJ,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAG,+BAA+B,+CAA+C;GAC9E,CAAA;EACF,CAAA"}
@@ -0,0 +1,95 @@
1
+ import { useOrgConsole as e } from "../../../providers/OrgConsoleProvider.js";
2
+ import { useMyOrganizations as t } from "../../organizations/hooks/useOrganizations.js";
3
+ import { useTr as n } from "../../../shared/hooks/useTr.js";
4
+ import { LoadingState as r } from "../../../shared/components/feedback/LoadingState.js";
5
+ import { ErrorState as i } from "../../../shared/components/feedback/ErrorState.js";
6
+ import { EmptyState as a } from "../../../shared/components/feedback/EmptyState.js";
7
+ import { useOrganizationPageView as o } from "../../../hooks/useOrganizationPageView.js";
8
+ import { useOrgsWithPermission as s } from "../../organizations/hooks/useOrgsWithPermission.js";
9
+ import { useEffect as c, useMemo as l, useState as u } from "react";
10
+ import { Building as d, ChevronRight as f, UserPlus as p } from "lucide-react";
11
+ import { jsx as m, jsxs as h } from "react/jsx-runtime";
12
+ //#region src/organization-management/modules/members/pages/InvitePeoplePage.tsx
13
+ function g() {
14
+ o("organizations.invite-people");
15
+ let g = n(), { navigate: _, basePath: v = "/organizations" } = e(), { organizations: y, loading: b, error: x } = t(100, 0), { allowedIds: S, loading: C, isError: w } = s(l(() => y.map((e) => e.id), [y]), "member", "create"), T = l(() => y.filter((e) => S.has(e.id)), [y, S]), [E, D] = u(!1);
16
+ return c(() => {
17
+ E || b || C || T.length === 1 && (D(!0), _(`${v}/${T[0].id}/members/invite`));
18
+ }, [
19
+ E,
20
+ b,
21
+ C,
22
+ T,
23
+ _,
24
+ v
25
+ ]), b || C ? /* @__PURE__ */ m("div", {
26
+ className: "min-h-full bg-[var(--color-bg-canvas)] p-6",
27
+ children: /* @__PURE__ */ m(r, { message: g("common.loading", "Loading...") })
28
+ }) : x || w ? /* @__PURE__ */ m("div", {
29
+ className: "min-h-full bg-[var(--color-bg-canvas)] p-6",
30
+ children: /* @__PURE__ */ m(i, { message: x?.message || g("members.invitePeople.error", "Failed to load organizations for invite.") })
31
+ }) : /* @__PURE__ */ m("div", {
32
+ className: "min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-300",
33
+ children: /* @__PURE__ */ h("div", {
34
+ className: "mx-auto max-w-3xl space-y-6",
35
+ children: [/* @__PURE__ */ h("div", {
36
+ className: "flex items-center gap-3",
37
+ children: [/* @__PURE__ */ m(p, {
38
+ size: 24,
39
+ className: "text-[var(--color-text-primary)]",
40
+ "aria-hidden": "true"
41
+ }), /* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("h1", {
42
+ className: "text-2xl font-bold text-[var(--color-text-primary)]",
43
+ children: g("members.invitePeople.title", "Invite People")
44
+ }), /* @__PURE__ */ m("p", {
45
+ className: "mt-1 text-sm text-[var(--color-text-secondary)]",
46
+ children: g("members.invitePeople.subtitle", "Pick an organization to send an invite into. You can also invite from any organization or workspace detail page.")
47
+ })] })]
48
+ }), T.length === 0 ? /* @__PURE__ */ m("div", {
49
+ className: "rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-6",
50
+ children: /* @__PURE__ */ m(a, {
51
+ icon: /* @__PURE__ */ m(p, {
52
+ size: 40,
53
+ "aria-hidden": "true"
54
+ }),
55
+ title: g("members.invitePeople.emptyTitle", "No organizations where you can invite"),
56
+ description: g("members.invitePeople.emptyDescription", "You don't currently have permission to invite members into any organization. Ask an organization admin to grant you invite access.")
57
+ })
58
+ }) : /* @__PURE__ */ m("ul", {
59
+ className: "divide-y divide-[var(--color-border-default)] rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] shadow-[var(--shadow-elevation-1)]",
60
+ children: T.map((e) => /* @__PURE__ */ m("li", { children: /* @__PURE__ */ h("button", {
61
+ type: "button",
62
+ onClick: () => _(`${v}/${e.id}/members/invite`),
63
+ className: "flex w-full items-center justify-between gap-4 px-5 py-4 text-left transition-colors hover:bg-[var(--color-bg-subtle)]",
64
+ children: [/* @__PURE__ */ h("div", {
65
+ className: "flex min-w-0 items-center gap-3",
66
+ children: [/* @__PURE__ */ m("div", {
67
+ className: "flex size-10 items-center justify-center rounded-lg bg-[var(--color-status-info-bgSubtle)] text-[var(--color-status-info-text)]",
68
+ children: /* @__PURE__ */ m(d, {
69
+ size: 20,
70
+ "aria-hidden": "true"
71
+ })
72
+ }), /* @__PURE__ */ h("div", {
73
+ className: "min-w-0",
74
+ children: [/* @__PURE__ */ m("p", {
75
+ className: "truncate text-sm font-semibold text-[var(--color-text-primary)]",
76
+ children: e.name
77
+ }), /* @__PURE__ */ h("p", {
78
+ className: "truncate font-mono text-xs text-[var(--color-text-secondary)]",
79
+ children: ["/", e.slug]
80
+ })]
81
+ })]
82
+ }), /* @__PURE__ */ m(f, {
83
+ size: 18,
84
+ className: "shrink-0 text-[var(--color-text-secondary)]",
85
+ "aria-hidden": "true"
86
+ })]
87
+ }) }, e.id))
88
+ })]
89
+ })
90
+ });
91
+ }
92
+ //#endregion
93
+ export { g as InvitePeoplePage };
94
+
95
+ //# sourceMappingURL=InvitePeoplePage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvitePeoplePage.js","names":[],"sources":["../../../../../src/organization-management/modules/members/pages/InvitePeoplePage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Building, ChevronRight, UserPlus } from 'lucide-react';\nimport { useOrgConsole } from '../../../providers/OrgConsoleProvider';\nimport { useMyOrganizations } from '../../organizations/hooks/useOrganizations';\nimport { useOrgsWithPermission } from '../../organizations/hooks/useOrgsWithPermission';\nimport { LoadingState } from '../../../shared/components/feedback/LoadingState';\nimport { ErrorState } from '../../../shared/components/feedback/ErrorState';\nimport { EmptyState } from '../../../shared/components/feedback/EmptyState';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport { useOrganizationPageView } from '../../../hooks/useOrganizationPageView';\n\n/**\n * InvitePeoplePage\n *\n * Globally-reachable entry point (sidebar nav item) for inviting users.\n * Lists the orgs the current user can invite into, then deep-links into\n * each org's existing `members/invite` page so the invite flow itself\n * remains a single source of truth.\n *\n * If the user has exactly one org with invite permission, redirect\n * straight to that org's invite page — saves a click for the common case.\n */\nexport function InvitePeoplePage() {\n useOrganizationPageView('organizations.invite-people');\n const tr = useTr();\n const { navigate, basePath = '/organizations' } = useOrgConsole();\n const { organizations, loading, error } = useMyOrganizations(100, 0);\n\n const orgIds = useMemo(() => organizations.map((o) => o.id), [organizations]);\n const {\n allowedIds,\n loading: permLoading,\n isError: permError,\n } = useOrgsWithPermission(orgIds, 'member', 'create');\n\n const invitable = useMemo(\n () => organizations.filter((o) => allowedIds.has(o.id)),\n [organizations, allowedIds]\n );\n\n const [autoRedirected, setAutoRedirected] = useState(false);\n useEffect(() => {\n if (autoRedirected) return;\n if (loading || permLoading) return;\n if (invitable.length === 1) {\n setAutoRedirected(true);\n navigate(`${basePath}/${invitable[0].id}/members/invite`);\n }\n }, [autoRedirected, loading, permLoading, invitable, navigate, basePath]);\n\n if (loading || permLoading) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <LoadingState message={tr('common.loading', 'Loading...')} />\n </div>\n );\n }\n\n if (error || permError) {\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] p-6\">\n <ErrorState\n message={\n error?.message ||\n tr('members.invitePeople.error', 'Failed to load organizations for invite.')\n }\n />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full bg-[var(--color-bg-canvas)] px-4 py-6 sm:px-6 animate-in fade-in duration-300\">\n <div className=\"mx-auto max-w-3xl space-y-6\">\n <div className=\"flex items-center gap-3\">\n <UserPlus size={24} className=\"text-[var(--color-text-primary)]\" aria-hidden=\"true\" />\n <div>\n <h1 className=\"text-2xl font-bold text-[var(--color-text-primary)]\">\n {tr('members.invitePeople.title', 'Invite People')}\n </h1>\n <p className=\"mt-1 text-sm text-[var(--color-text-secondary)]\">\n {tr(\n 'members.invitePeople.subtitle',\n 'Pick an organization to send an invite into. You can also invite from any organization or workspace detail page.'\n )}\n </p>\n </div>\n </div>\n\n {invitable.length === 0 ? (\n <div className=\"rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-6\">\n <EmptyState\n icon={<UserPlus size={40} aria-hidden=\"true\" />}\n title={tr('members.invitePeople.emptyTitle', 'No organizations where you can invite')}\n description={tr(\n 'members.invitePeople.emptyDescription',\n \"You don't currently have permission to invite members into any organization. Ask an organization admin to grant you invite access.\"\n )}\n />\n </div>\n ) : (\n <ul className=\"divide-y divide-[var(--color-border-default)] rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] shadow-[var(--shadow-elevation-1)]\">\n {invitable.map((org) => (\n <li key={org.id}>\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/${org.id}/members/invite`)}\n className=\"flex w-full items-center justify-between gap-4 px-5 py-4 text-left transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex size-10 items-center justify-center rounded-lg bg-[var(--color-status-info-bgSubtle)] text-[var(--color-status-info-text)]\">\n <Building size={20} aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-[var(--color-text-primary)]\">\n {org.name}\n </p>\n <p className=\"truncate font-mono text-xs text-[var(--color-text-secondary)]\">\n /{org.slug}\n </p>\n </div>\n </div>\n <ChevronRight\n size={18}\n className=\"shrink-0 text-[var(--color-text-secondary)]\"\n aria-hidden=\"true\"\n />\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAsBA,SAAgB,IAAmB;AACjC,GAAwB,8BAA8B;CACtD,IAAM,IAAK,GAAO,EACZ,EAAE,aAAU,cAAW,qBAAqB,GAAe,EAC3D,EAAE,kBAAe,YAAS,aAAU,EAAmB,KAAK,EAAE,EAG9D,EACJ,eACA,SAAS,GACT,SAAS,MACP,EALW,QAAc,EAAc,KAAK,MAAM,EAAE,GAAG,EAAE,CAAC,EAAc,CAAC,EAK3C,UAAU,SAAS,EAE/C,IAAY,QACV,EAAc,QAAQ,MAAM,EAAW,IAAI,EAAE,GAAG,CAAC,EACvD,CAAC,GAAe,EAAW,CAC5B,EAEK,CAAC,GAAgB,KAAqB,EAAS,GAAM;AA+B3D,QA9BA,QAAgB;AACV,OACA,KAAW,KACX,EAAU,WAAW,MACvB,EAAkB,GAAK,EACvB,EAAS,GAAG,EAAS,GAAG,EAAU,GAAG,GAAG,iBAAiB;IAE1D;EAAC;EAAgB;EAAS;EAAa;EAAW;EAAU;EAAS,CAAC,EAErE,KAAW,IAEX,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAc,SAAS,EAAG,kBAAkB,aAAa,EAAI,CAAA;EACzD,CAAA,GAIN,KAAS,IAET,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EACE,SACE,GAAO,WACP,EAAG,8BAA8B,2CAA2C,EAE9E,CAAA;EACE,CAAA,GAKR,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAU,MAAM;KAAI,WAAU;KAAmC,eAAY;KAAS,CAAA,EACtF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,8BAA8B,gBAAgB;KAC/C,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,iCACA,mHACD;KACC,CAAA,CACA,EAAA,CAAA,CACF;OAEL,EAAU,WAAW,IACpB,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD;MAAU,MAAM;MAAI,eAAY;MAAS,CAAA;KAC/C,OAAO,EAAG,mCAAmC,wCAAwC;KACrF,aAAa,EACX,yCACA,qIACD;KACD,CAAA;IACE,CAAA,GAEN,kBAAC,MAAD;IAAI,WAAU;cACX,EAAU,KAAK,MACd,kBAAC,MAAD,EAAA,UACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,GAAG,EAAS,GAAG,EAAI,GAAG,iBAAiB;KAC/D,WAAU;eAHZ,CAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAU,MAAM;QAAI,eAAY;QAAS,CAAA;OACrC,CAAA,EACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI;QACH,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBAAb,CAA6E,KACzE,EAAI,KACJ;UACA;SACF;SACN,kBAAC,GAAD;MACE,MAAM;MACN,WAAU;MACV,eAAY;MACZ,CAAA,CACK;QACN,EAzBI,EAAI,GAyBR,CACL;IACC,CAAA,CAEH;;EACF,CAAA"}
@@ -0,0 +1,52 @@
1
+ import { UserRole as e } from "../../../../generated/global-types.js";
2
+ //#region src/organization-management/modules/members/utils/bulkInviteCsv.ts
3
+ var t = new Set([e.Member, e.OrgAdmin]), n = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4
+ function r(e) {
5
+ let t = [], n = "", r = !1;
6
+ for (let i = 0; i < e.length; i += 1) {
7
+ let a = e[i];
8
+ r ? a === "\"" ? e[i + 1] === "\"" ? (n += "\"", i += 1) : r = !1 : n += a : a === "\"" ? r = !0 : a === "," ? (t.push(n), n = "") : n += a;
9
+ }
10
+ return t.push(n), t.map((e) => e.trim());
11
+ }
12
+ function i(e) {
13
+ let i = e.replace(/^\uFEFF/, "").split(/\r?\n/).filter((e) => e.trim().length > 0);
14
+ if (i.length === 0) return [];
15
+ let a = r(i[0]).map((e) => e.toLowerCase()), o = a.indexOf("email");
16
+ if (o === -1) return [{
17
+ rowNumber: 1,
18
+ email: "",
19
+ errors: ["CSV header must include an \"email\" column."]
20
+ }];
21
+ let s = a.indexOf("role"), c = a.indexOf("message"), l = /* @__PURE__ */ new Set();
22
+ return i.slice(1).map((e, i) => {
23
+ let a = r(e), u = i + 2, d = (a[o] ?? "").toLowerCase(), f = s >= 0 ? (a[s] ?? "").toUpperCase() : "", p = c >= 0 ? a[c] ?? "" : "", m = [];
24
+ d ? n.test(d) ? l.has(d) ? m.push("Duplicate email in this CSV.") : l.add(d) : m.push("Invalid email format.") : m.push("Email is required.");
25
+ let h;
26
+ return f && (t.has(f) ? h = f : m.push(`Invalid role "${f}". Use MEMBER or ORG_ADMIN.`)), {
27
+ rowNumber: u,
28
+ email: d,
29
+ role: h,
30
+ message: p || void 0,
31
+ errors: m
32
+ };
33
+ });
34
+ }
35
+ var a = "invite-members-template.csv", o = [
36
+ "email,role,message",
37
+ "alice@example.com,MEMBER,Welcome to the team",
38
+ "bob@example.com,ORG_ADMIN,",
39
+ "carol@example.com,,"
40
+ ].join("\n");
41
+ function s(e, t) {
42
+ let n = e instanceof Error ? e.message : String(e);
43
+ return n.includes("CONFLICT") ? t("members.invite.error.conflict", "An invite already exists for this email address.") : n.includes("already a member") ? t("members.invite.error.alreadyMember", "This user is already a member of the organization.") : n;
44
+ }
45
+ function c() {
46
+ let e = new Blob([o], { type: "text/csv;charset=utf-8" }), t = URL.createObjectURL(e), n = document.createElement("a");
47
+ n.href = t, n.download = a, document.body.appendChild(n), n.click(), document.body.removeChild(n), URL.revokeObjectURL(t);
48
+ }
49
+ //#endregion
50
+ export { c as downloadTemplate, s as formatInviteError, i as parseBulkInviteCsv };
51
+
52
+ //# sourceMappingURL=bulkInviteCsv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bulkInviteCsv.js","names":[],"sources":["../../../../../src/organization-management/modules/members/utils/bulkInviteCsv.ts"],"sourcesContent":["/**\n * Minimal CSV parsing + template generation for bulk member invites.\n * Hand-rolled (no csv lib dep) — invite CSVs are small enough to keep\n * the parser inline. Supports double-quoted fields with embedded commas\n * and escaped quotes (RFC 4180 subset).\n *\n * Expected columns (header row required, case-insensitive, any order):\n * email — required\n * role — optional, one of MEMBER | ADMIN | OWNER (case-insensitive)\n * message — optional, free text\n */\n\nimport { UserRole } from '../../../../generated/global-types';\n\nexport interface BulkInviteRow {\n rowNumber: number;\n email: string;\n role?: UserRole;\n message?: string;\n errors: string[];\n}\n\nconst VALID_ROLES = new Set<string>([UserRole.Member, UserRole.OrgAdmin]);\n\nconst EMAIL_RE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/**\n * RFC 4180-subset CSV row parser. Handles double-quoted fields, embedded\n * commas and `\"\"` escapes. Trims surrounding whitespace per field but\n * preserves whitespace inside quotes.\n */\nfunction splitCsvLine(line: string): string[] {\n const fields: string[] = [];\n let current = '';\n let inQuotes = false;\n for (let i = 0; i < line.length; i += 1) {\n const ch = line[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (line[i + 1] === '\"') {\n current += '\"';\n i += 1;\n } else {\n inQuotes = false;\n }\n } else {\n current += ch;\n }\n } else if (ch === '\"') {\n inQuotes = true;\n } else if (ch === ',') {\n fields.push(current);\n current = '';\n } else {\n current += ch;\n }\n }\n fields.push(current);\n return fields.map((f) => f.trim());\n}\n\nexport function parseBulkInviteCsv(text: string): BulkInviteRow[] {\n const lines = text\n .replace(/^\\uFEFF/, '')\n .split(/\\r?\\n/)\n .filter((l) => l.trim().length > 0);\n\n if (lines.length === 0) return [];\n\n const header = splitCsvLine(lines[0]).map((h) => h.toLowerCase());\n const emailIdx = header.indexOf('email');\n if (emailIdx === -1) {\n return [\n {\n rowNumber: 1,\n email: '',\n errors: ['CSV header must include an \"email\" column.'],\n },\n ];\n }\n const roleIdx = header.indexOf('role');\n const messageIdx = header.indexOf('message');\n\n const seen = new Set<string>();\n\n return lines.slice(1).map((line, idx) => {\n const fields = splitCsvLine(line);\n const rowNumber = idx + 2;\n const email = (fields[emailIdx] ?? '').toLowerCase();\n const rawRole = roleIdx >= 0 ? (fields[roleIdx] ?? '').toUpperCase() : '';\n const message = messageIdx >= 0 ? (fields[messageIdx] ?? '') : '';\n const errors: string[] = [];\n\n if (!email) {\n errors.push('Email is required.');\n } else if (!EMAIL_RE.test(email)) {\n errors.push('Invalid email format.');\n } else if (seen.has(email)) {\n errors.push('Duplicate email in this CSV.');\n } else {\n seen.add(email);\n }\n\n let role: UserRole | undefined;\n if (rawRole) {\n if (VALID_ROLES.has(rawRole)) {\n role = rawRole as UserRole;\n } else {\n errors.push(`Invalid role \"${rawRole}\". Use MEMBER or ORG_ADMIN.`);\n }\n }\n\n return {\n rowNumber,\n email,\n role,\n message: message || undefined,\n errors,\n };\n });\n}\n\n/**\n * Sample template content + filename. Includes a header row, a couple of\n * placeholder example rows, and a comment-style hint as a trailing row\n * users will likely delete.\n */\nexport const TEMPLATE_FILENAME = 'invite-members-template.csv';\n\nexport const TEMPLATE_CONTENT = [\n 'email,role,message',\n 'alice@example.com,MEMBER,Welcome to the team',\n 'bob@example.com,ORG_ADMIN,',\n 'carol@example.com,,',\n].join('\\n');\n\n/**\n * Map a thrown invite mutation error to a user-facing message, matching the\n * existing single-invite page's mapping (CONFLICT → already invited, etc).\n * Centralised here so single + bulk surfaces stay consistent.\n */\nexport function formatInviteError(\n error: unknown,\n tr: (key: string, fallback: string) => string\n): string {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('CONFLICT')) {\n return tr('members.invite.error.conflict', 'An invite already exists for this email address.');\n }\n if (message.includes('already a member')) {\n return tr(\n 'members.invite.error.alreadyMember',\n 'This user is already a member of the organization.'\n );\n }\n return message;\n}\n\nexport function downloadTemplate(): void {\n const blob = new Blob([TEMPLATE_CONTENT], { type: 'text/csv;charset=utf-8' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = TEMPLATE_FILENAME;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n"],"mappings":";;AAsBA,IAAM,IAAc,IAAI,IAAY,CAAC,EAAS,QAAQ,EAAS,SAAS,CAAC,EAEnE,IAAW;AAOjB,SAAS,EAAa,GAAwB;CAC5C,IAAM,IAAmB,EAAE,EACvB,IAAU,IACV,IAAW;AACf,MAAK,IAAI,IAAI,GAAG,IAAI,EAAK,QAAQ,KAAK,GAAG;EACvC,IAAM,IAAK,EAAK;AAChB,EAAI,IACE,MAAO,OACL,EAAK,IAAI,OAAO,QAClB,KAAW,MACX,KAAK,KAEL,IAAW,KAGb,KAAW,IAEJ,MAAO,OAChB,IAAW,KACF,MAAO,OAChB,EAAO,KAAK,EAAQ,EACpB,IAAU,MAEV,KAAW;;AAIf,QADA,EAAO,KAAK,EAAQ,EACb,EAAO,KAAK,MAAM,EAAE,MAAM,CAAC;;AAGpC,SAAgB,EAAmB,GAA+B;CAChE,IAAM,IAAQ,EACX,QAAQ,WAAW,GAAG,CACtB,MAAM,QAAQ,CACd,QAAQ,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE;AAErC,KAAI,EAAM,WAAW,EAAG,QAAO,EAAE;CAEjC,IAAM,IAAS,EAAa,EAAM,GAAG,CAAC,KAAK,MAAM,EAAE,aAAa,CAAC,EAC3D,IAAW,EAAO,QAAQ,QAAQ;AACxC,KAAI,MAAa,GACf,QAAO,CACL;EACE,WAAW;EACX,OAAO;EACP,QAAQ,CAAC,+CAA6C;EACvD,CACF;CAEH,IAAM,IAAU,EAAO,QAAQ,OAAO,EAChC,IAAa,EAAO,QAAQ,UAAU,EAEtC,oBAAO,IAAI,KAAa;AAE9B,QAAO,EAAM,MAAM,EAAE,CAAC,KAAK,GAAM,MAAQ;EACvC,IAAM,IAAS,EAAa,EAAK,EAC3B,IAAY,IAAM,GAClB,KAAS,EAAO,MAAa,IAAI,aAAa,EAC9C,IAAU,KAAW,KAAK,EAAO,MAAY,IAAI,aAAa,GAAG,IACjE,IAAU,KAAc,IAAK,EAAO,MAAe,KAAM,IACzD,IAAmB,EAAE;AAE3B,EAAK,IAEO,EAAS,KAAK,EAAM,GAErB,EAAK,IAAI,EAAM,GACxB,EAAO,KAAK,+BAA+B,GAE3C,EAAK,IAAI,EAAM,GAJf,EAAO,KAAK,wBAAwB,GAFpC,EAAO,KAAK,qBAAqB;EASnC,IAAI;AASJ,SARI,MACE,EAAY,IAAI,EAAQ,GAC1B,IAAO,IAEP,EAAO,KAAK,iBAAiB,EAAQ,6BAA6B,GAI/D;GACL;GACA;GACA;GACA,SAAS,KAAW,KAAA;GACpB;GACD;GACD;;AAQJ,IAAa,IAAoB,+BAEpB,IAAmB;CAC9B;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;AAOZ,SAAgB,EACd,GACA,GACQ;CACR,IAAM,IAAU,aAAiB,QAAQ,EAAM,UAAU,OAAO,EAAM;AAUtE,QATI,EAAQ,SAAS,WAAW,GACvB,EAAG,iCAAiC,mDAAmD,GAE5F,EAAQ,SAAS,mBAAmB,GAC/B,EACL,sCACA,qDACD,GAEI;;AAGT,SAAgB,IAAyB;CACvC,IAAM,IAAO,IAAI,KAAK,CAAC,EAAiB,EAAE,EAAE,MAAM,0BAA0B,CAAC,EACvE,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAI,SAAS,cAAc,IAAI;AAMrC,CALA,EAAE,OAAO,GACT,EAAE,WAAW,GACb,SAAS,KAAK,YAAY,EAAE,EAC5B,EAAE,OAAO,EACT,SAAS,KAAK,YAAY,EAAE,EAC5B,IAAI,gBAAgB,EAAI"}