@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
@@ -1,44 +1,55 @@
1
1
  import { OrgConsoleProvider as e } from "./providers/OrgConsoleProvider.js";
2
2
  import { UserOrganizationRoutes as t } from "./UserOrganizationRoutes.js";
3
3
  import { useEffect as n } from "react";
4
- import { Building2 as r, Mail as i } from "lucide-react";
5
- import { Toaster as a } from "@burdenoff/fe-libs/ui";
6
- import { FeatureFlagProvider as o } from "@burdenoff/fe-libs/shared/feature-flags";
7
- import { jsx as s, jsxs as c } from "react/jsx-runtime";
4
+ import { Building2 as r, Mail as i, UserPlus as a } from "lucide-react";
5
+ import { Toaster as o } from "@burdenoff/fe-libs/ui";
6
+ import { FeatureFlagProvider as s } from "@burdenoff/fe-libs/shared/feature-flags";
7
+ import { jsx as c, jsxs as l } from "react/jsx-runtime";
8
8
  //#region src/organization-management/UserOrganizationRoot.tsx
9
- var l = (l) => {
10
- let { registerNavItems: u, registerFooterItems: d, basePath: f = "/organizations" } = l, p = (e, t) => (e.endsWith("/") ? e.slice(0, -1) : e) + (t.startsWith("/") ? t : `/${t}`);
9
+ var u = (u) => {
10
+ let { registerNavItems: d, registerFooterItems: f, basePath: p = "/organizations" } = u, m = (e, t) => (e.endsWith("/") ? e.slice(0, -1) : e) + (t.startsWith("/") ? t : `/${t}`);
11
11
  return n(() => {
12
- if (u) return u([{
13
- id: "user-org-organizations",
14
- label: "Organizations",
15
- path: f,
16
- icon: /* @__PURE__ */ s(r, { size: 20 }),
17
- order: 20,
18
- group: "user"
19
- }, {
20
- id: "user-org-invites",
21
- label: "My Invites",
22
- path: p(f, "/invites"),
23
- icon: /* @__PURE__ */ s(i, { size: 20 }),
24
- order: 30,
25
- group: "user"
26
- }]);
27
- }, [u, f]), n(() => {
28
- if (d) return d([{
12
+ if (d) return d([
13
+ {
14
+ id: "user-org-organizations",
15
+ label: "Organizations",
16
+ path: p,
17
+ icon: /* @__PURE__ */ c(r, { size: 20 }),
18
+ order: 20,
19
+ group: "user"
20
+ },
21
+ {
22
+ id: "user-org-invite-people",
23
+ label: "Invite People",
24
+ path: m(p, "/invite"),
25
+ icon: /* @__PURE__ */ c(a, { size: 20 }),
26
+ order: 25,
27
+ group: "user"
28
+ },
29
+ {
30
+ id: "user-org-invites",
31
+ label: "My Invites",
32
+ path: m(p, "/invites"),
33
+ icon: /* @__PURE__ */ c(i, { size: 20 }),
34
+ order: 30,
35
+ group: "user"
36
+ }
37
+ ]);
38
+ }, [d, p]), n(() => {
39
+ if (f) return f([{
29
40
  id: "user-organization-version",
30
41
  label: "Organization (User) v0.1.0",
31
42
  type: "status",
32
43
  order: 30,
33
44
  section: "left"
34
45
  }]);
35
- }, [d]), /* @__PURE__ */ s(o, {
46
+ }, [f]), /* @__PURE__ */ c(s, {
36
47
  workspaceId: null,
37
48
  gateway: "global",
38
49
  defaultEnabled: !0,
39
- children: /* @__PURE__ */ c(e, {
40
- ...l,
41
- children: [/* @__PURE__ */ s(t, {}), /* @__PURE__ */ s(a, {
50
+ children: /* @__PURE__ */ l(e, {
51
+ ...u,
52
+ children: [/* @__PURE__ */ c(t, {}), /* @__PURE__ */ c(o, {
42
53
  position: "top-right",
43
54
  richColors: !0,
44
55
  closeButton: !0
@@ -47,6 +58,6 @@ var l = (l) => {
47
58
  });
48
59
  };
49
60
  //#endregion
50
- export { l as UserOrganizationRoot };
61
+ export { u as UserOrganizationRoot };
51
62
 
52
63
  //# sourceMappingURL=UserOrganizationRoot.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"UserOrganizationRoot.js","names":[],"sources":["../../src/organization-management/UserOrganizationRoot.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useEffect } from 'react';\nimport { Building2, Mail } from 'lucide-react';\nimport { Toaster } from '@burdenoff/fe-libs/ui';\nimport { FeatureFlagProvider } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { OrgConsoleProvider } from './providers/OrgConsoleProvider';\nimport type { NavItem, OrganizationRootProps } from './types';\nimport { UserOrganizationRoutes } from './UserOrganizationRoutes';\n\n/**\n * UserOrganizationRoot\n *\n * User-facing root for the organization management MFE, intended to be\n * mounted inside product shells (workspaces, botlit, fluidgrids, etc.).\n *\n * Exposes the membership-scoped organization surface:\n * - Organizations list (`mode=\"my\"` → `myOrganizations`)\n * - Organization detail / edit (per-action `hasPermission` gates;\n * backend enforces membership on `organization(id)` resolver)\n * - Members list / detail / invite (backend enforces membership on\n * `organizationMembers` resolver)\n * - My pending invites\n *\n * Registers \"Organizations\" + \"My Invites\" nav items.\n *\n * Takes the same props as `OrganizationRootProps` so hosts can swap\n * between the user and admin roots without adapter code.\n */\nexport const UserOrganizationRoot: FC<OrganizationRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = '/organizations' } = props;\n\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n {\n id: 'user-org-organizations',\n label: 'Organizations',\n path: basePath,\n icon: <Building2 size={20} />,\n order: 20,\n group: 'user',\n },\n {\n id: 'user-org-invites',\n label: 'My Invites',\n path: joinPath(basePath, '/invites'),\n icon: <Mail size={20} />,\n order: 30,\n group: 'user',\n },\n ];\n\n const cleanup = registerNavItems(navItems);\n return cleanup;\n }, [registerNavItems, basePath]);\n\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: 'user-organization-version',\n label: 'Organization (User) v0.1.0',\n type: 'status' as const,\n order: 30,\n section: 'left' as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems]);\n\n return (\n <FeatureFlagProvider workspaceId={null} gateway=\"global\" defaultEnabled={true}>\n <OrgConsoleProvider {...props}>\n <UserOrganizationRoutes />\n <Toaster position=\"top-right\" richColors closeButton />\n </OrgConsoleProvider>\n </FeatureFlagProvider>\n );\n};\n"],"mappings":";;;;;;;;AA4BA,IAAa,KAAmD,MAAU;CACxE,IAAM,EAAE,qBAAkB,wBAAqB,cAAW,qBAAqB,GAEzE,KAAY,GAAc,OACZ,EAAK,SAAS,IAAI,GAAG,EAAK,MAAM,GAAG,GAAG,GAAG,MACzC,EAAK,WAAW,IAAI,GAAG,IAAO,IAAI;AA+CtD,QA3CA,QAAgB;AACT,QAsBL,QADgB,EAnBY,CAC1B;GACE,IAAI;GACJ,OAAO;GACP,MAAM;GACN,MAAM,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;GAC7B,OAAO;GACP,OAAO;GACR,EACD;GACE,IAAI;GACJ,OAAO;GACP,MAAM,EAAS,GAAU,WAAW;GACpC,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;GACxB,OAAO;GACP,OAAO;GACR,CACF,CAEyC;IAEzC,CAAC,GAAkB,EAAS,CAAC,EAEhC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO;GACP,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,EAAoB,CAAC,EAGvB,kBAAC,GAAD;EAAqB,aAAa;EAAM,SAAQ;EAAS,gBAAgB;YACvE,kBAAC,GAAD;GAAoB,GAAI;aAAxB,CACE,kBAAC,GAAD,EAA0B,CAAA,EAC1B,kBAAC,GAAD;IAAS,UAAS;IAAY,YAAA;IAAW,aAAA;IAAc,CAAA,CACpC;;EACD,CAAA"}
1
+ {"version":3,"file":"UserOrganizationRoot.js","names":[],"sources":["../../src/organization-management/UserOrganizationRoot.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useEffect } from 'react';\nimport { Building2, Mail, UserPlus } from 'lucide-react';\nimport { Toaster } from '@burdenoff/fe-libs/ui';\nimport { FeatureFlagProvider } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { OrgConsoleProvider } from './providers/OrgConsoleProvider';\nimport type { NavItem, OrganizationRootProps } from './types';\nimport { UserOrganizationRoutes } from './UserOrganizationRoutes';\n\n/**\n * UserOrganizationRoot\n *\n * User-facing root for the organization management MFE, intended to be\n * mounted inside product shells (workspaces, botlit, fluidgrids, etc.).\n *\n * Exposes the membership-scoped organization surface:\n * - Organizations list (`mode=\"my\"` → `myOrganizations`)\n * - Organization detail / edit (per-action `hasPermission` gates;\n * backend enforces membership on `organization(id)` resolver)\n * - Members list / detail / invite (backend enforces membership on\n * `organizationMembers` resolver)\n * - My pending invites\n *\n * Registers \"Organizations\" + \"My Invites\" nav items.\n *\n * Takes the same props as `OrganizationRootProps` so hosts can swap\n * between the user and admin roots without adapter code.\n */\nexport const UserOrganizationRoot: FC<OrganizationRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = '/organizations' } = props;\n\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n {\n id: 'user-org-organizations',\n label: 'Organizations',\n path: basePath,\n icon: <Building2 size={20} />,\n order: 20,\n group: 'user',\n },\n {\n id: 'user-org-invite-people',\n label: 'Invite People',\n path: joinPath(basePath, '/invite'),\n icon: <UserPlus size={20} />,\n order: 25,\n group: 'user',\n },\n {\n id: 'user-org-invites',\n label: 'My Invites',\n path: joinPath(basePath, '/invites'),\n icon: <Mail size={20} />,\n order: 30,\n group: 'user',\n },\n ];\n\n const cleanup = registerNavItems(navItems);\n return cleanup;\n }, [registerNavItems, basePath]);\n\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: 'user-organization-version',\n label: 'Organization (User) v0.1.0',\n type: 'status' as const,\n order: 30,\n section: 'left' as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems]);\n\n return (\n <FeatureFlagProvider workspaceId={null} gateway=\"global\" defaultEnabled={true}>\n <OrgConsoleProvider {...props}>\n <UserOrganizationRoutes />\n <Toaster position=\"top-right\" richColors closeButton />\n </OrgConsoleProvider>\n </FeatureFlagProvider>\n );\n};\n"],"mappings":";;;;;;;;AA4BA,IAAa,KAAmD,MAAU;CACxE,IAAM,EAAE,qBAAkB,wBAAqB,cAAW,qBAAqB,GAEzE,KAAY,GAAc,OACZ,EAAK,SAAS,IAAI,GAAG,EAAK,MAAM,GAAG,GAAG,GAAG,MACzC,EAAK,WAAW,IAAI,GAAG,IAAO,IAAI;AAuDtD,QAnDA,QAAgB;AACT,QA8BL,QADgB,EA3BY;GAC1B;IACE,IAAI;IACJ,OAAO;IACP,MAAM;IACN,MAAM,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;IAC7B,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO;IACP,MAAM,EAAS,GAAU,UAAU;IACnC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;IAC5B,OAAO;IACP,OAAO;IACR;GACD;IACE,IAAI;IACJ,OAAO;IACP,MAAM,EAAS,GAAU,WAAW;IACpC,MAAM,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA;IACxB,OAAO;IACP,OAAO;IACR;GACF,CAEyC;IAEzC,CAAC,GAAkB,EAAS,CAAC,EAEhC,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO;GACP,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,EAAoB,CAAC,EAGvB,kBAAC,GAAD;EAAqB,aAAa;EAAM,SAAQ;EAAS,gBAAgB;YACvE,kBAAC,GAAD;GAAoB,GAAI;aAAxB,CACE,kBAAC,GAAD,EAA0B,CAAA,EAC1B,kBAAC,GAAD;IAAS,UAAS;IAAY,YAAA;IAAW,aAAA;IAAc,CAAA,CACpC;;EACD,CAAA"}
@@ -4,23 +4,23 @@ import { Suspense as n, lazy as r } from "react";
4
4
  import { jsx as i, jsxs as a } from "react/jsx-runtime";
5
5
  import { Outlet as o, Route as s, Routes as c, useParams as l } from "react-router-dom";
6
6
  //#region src/organization-management/UserOrganizationRoutes.tsx
7
- var u = r(() => import("./modules/organizations/pages/OrganizationCreatePage.js").then((e) => ({ default: e.OrganizationCreatePage }))), d = r(() => import("./modules/organizations/pages/OrganizationDetailPage.js").then((e) => ({ default: e.OrganizationDetailPage }))), f = r(() => import("./modules/organizations/pages/OrganizationEditPage.js").then((e) => ({ default: e.OrganizationEditPage }))), p = r(() => import("./modules/workspaces/pages/WorkspacesListPage.js").then((e) => ({ default: e.WorkspacesListPage }))), m = r(() => import("./modules/workspaces/pages/WorkspaceCreatePage.js").then((e) => ({ default: e.WorkspaceCreatePage }))), h = r(() => import("./modules/workspaces/pages/WorkspaceDetailPage.js").then((e) => ({ default: e.WorkspaceDetailPage }))), g = r(() => import("./modules/workspaces/pages/WorkspaceEditPage.js").then((e) => ({ default: e.WorkspaceEditPage }))), _ = r(() => import("./modules/members/pages/MembersListPage.js").then((e) => ({ default: e.MembersListPage }))), v = r(() => import("./modules/members/pages/MemberDetailPage.js").then((e) => ({ default: e.MemberDetailPage }))), y = r(() => import("./modules/members/pages/InviteMemberPage.js").then((e) => ({ default: e.InviteMemberPage }))), b = r(() => import("./modules/members/pages/MyInvitesPage.js").then((e) => ({ default: e.MyInvitesPage })));
8
- function x({ paramName: e }) {
7
+ var u = r(() => import("./modules/organizations/pages/OrganizationCreatePage.js").then((e) => ({ default: e.OrganizationCreatePage }))), d = r(() => import("./modules/organizations/pages/OrganizationDetailPage.js").then((e) => ({ default: e.OrganizationDetailPage }))), f = r(() => import("./modules/organizations/pages/OrganizationEditPage.js").then((e) => ({ default: e.OrganizationEditPage }))), p = r(() => import("./modules/workspaces/pages/WorkspacesListPage.js").then((e) => ({ default: e.WorkspacesListPage }))), m = r(() => import("./modules/workspaces/pages/WorkspaceCreatePage.js").then((e) => ({ default: e.WorkspaceCreatePage }))), h = r(() => import("./modules/workspaces/pages/WorkspaceDetailPage.js").then((e) => ({ default: e.WorkspaceDetailPage }))), g = r(() => import("./modules/workspaces/pages/WorkspaceEditPage.js").then((e) => ({ default: e.WorkspaceEditPage }))), _ = r(() => import("./modules/members/pages/MembersListPage.js").then((e) => ({ default: e.MembersListPage }))), v = r(() => import("./modules/members/pages/MemberDetailPage.js").then((e) => ({ default: e.MemberDetailPage }))), y = r(() => import("./modules/members/pages/InviteMemberPage.js").then((e) => ({ default: e.InviteMemberPage }))), b = r(() => import("./modules/members/pages/MyInvitesPage.js").then((e) => ({ default: e.MyInvitesPage }))), x = r(() => import("./modules/members/pages/InvitePeoplePage.js").then((e) => ({ default: e.InvitePeoplePage })));
8
+ function S({ paramName: e }) {
9
9
  let n = l()[e];
10
10
  return n ? /* @__PURE__ */ i(t, {
11
11
  organizationId: n,
12
12
  children: /* @__PURE__ */ i(o, {})
13
13
  }) : /* @__PURE__ */ i(o, {});
14
14
  }
15
- function S() {
15
+ function C() {
16
16
  return /* @__PURE__ */ i("div", {
17
17
  className: "flex items-center justify-center min-h-[200px]",
18
18
  children: /* @__PURE__ */ i("div", { className: "size-8 animate-spin rounded-full border-2 border-[var(--color-border-default)] border-t-[var(--color-action-primary-bg)]" })
19
19
  });
20
20
  }
21
- function C() {
21
+ function w() {
22
22
  return /* @__PURE__ */ i(n, {
23
- fallback: /* @__PURE__ */ i(S, {}),
23
+ fallback: /* @__PURE__ */ i(C, {}),
24
24
  children: /* @__PURE__ */ a(c, { children: [
25
25
  /* @__PURE__ */ i(s, {
26
26
  index: !0,
@@ -32,7 +32,7 @@ function C() {
32
32
  }),
33
33
  /* @__PURE__ */ a(s, {
34
34
  path: ":id",
35
- element: /* @__PURE__ */ i(x, { paramName: "id" }),
35
+ element: /* @__PURE__ */ i(S, { paramName: "id" }),
36
36
  children: [/* @__PURE__ */ i(s, {
37
37
  index: !0,
38
38
  element: /* @__PURE__ */ i(d, {})
@@ -45,6 +45,10 @@ function C() {
45
45
  path: "invites",
46
46
  element: /* @__PURE__ */ i(b, {})
47
47
  }),
48
+ /* @__PURE__ */ i(s, {
49
+ path: "invite",
50
+ element: /* @__PURE__ */ i(x, {})
51
+ }),
48
52
  /* @__PURE__ */ i(s, {
49
53
  path: "workspaces",
50
54
  element: /* @__PURE__ */ i(p, {})
@@ -63,7 +67,7 @@ function C() {
63
67
  }),
64
68
  /* @__PURE__ */ a(s, {
65
69
  path: ":organizationId",
66
- element: /* @__PURE__ */ i(x, { paramName: "organizationId" }),
70
+ element: /* @__PURE__ */ i(S, { paramName: "organizationId" }),
67
71
  children: [
68
72
  /* @__PURE__ */ i(s, {
69
73
  path: "members/invite",
@@ -83,6 +87,6 @@ function C() {
83
87
  });
84
88
  }
85
89
  //#endregion
86
- export { C as UserOrganizationRoutes };
90
+ export { w as UserOrganizationRoutes };
87
91
 
88
92
  //# sourceMappingURL=UserOrganizationRoutes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"UserOrganizationRoutes.js","names":[],"sources":["../../src/organization-management/UserOrganizationRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Outlet, Route, Routes, useParams } from 'react-router-dom';\n\n// Eager load — main entry point page\nimport { OrganizationsListPage } from './modules/organizations/pages/OrganizationsListPage';\nimport { OrgScopedPermissions } from './providers/OrgScopedPermissions';\n\n// Lazy load — secondary pages. Same set as the admin surface but the list\n// page below renders in `mode=\"my\"` (membership-scoped), and per-action\n// permission gates inside each page (hasPermission(PERMISSIONS.ORG_*))\n// already hide buttons the user can't use.\nconst OrganizationCreatePage = lazy(() =>\n import('./modules/organizations/pages/OrganizationCreatePage').then((m) => ({\n default: m.OrganizationCreatePage,\n }))\n);\nconst OrganizationDetailPage = lazy(() =>\n import('./modules/organizations/pages/OrganizationDetailPage').then((m) => ({\n default: m.OrganizationDetailPage,\n }))\n);\nconst OrganizationEditPage = lazy(() =>\n import('./modules/organizations/pages/OrganizationEditPage').then((m) => ({\n default: m.OrganizationEditPage,\n }))\n);\nconst WorkspacesListPage = lazy(() =>\n import('./modules/workspaces/pages/WorkspacesListPage').then((m) => ({\n default: m.WorkspacesListPage,\n }))\n);\nconst WorkspaceCreatePage = lazy(() =>\n import('./modules/workspaces/pages/WorkspaceCreatePage').then((m) => ({\n default: m.WorkspaceCreatePage,\n }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./modules/workspaces/pages/WorkspaceDetailPage').then((m) => ({\n default: m.WorkspaceDetailPage,\n }))\n);\nconst WorkspaceEditPage = lazy(() =>\n import('./modules/workspaces/pages/WorkspaceEditPage').then((m) => ({\n default: m.WorkspaceEditPage,\n }))\n);\nconst MembersListPage = lazy(() =>\n import('./modules/members/pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst MemberDetailPage = lazy(() =>\n import('./modules/members/pages/MemberDetailPage').then((m) => ({ default: m.MemberDetailPage }))\n);\nconst InviteMemberPage = lazy(() =>\n import('./modules/members/pages/InviteMemberPage').then((m) => ({ default: m.InviteMemberPage }))\n);\nconst MyInvitesPage = lazy(() =>\n import('./modules/members/pages/MyInvitesPage').then((m) => ({ default: m.MyInvitesPage }))\n);\n\n/**\n * Layout wrapper that scopes the PermissionProvider to the current\n * organization's id (read from the URL). This layout owns the `:id` or\n * `:organizationId` URL segment directly (see route declarations below),\n * so `useParams()[paramName]` is guaranteed to be populated and the\n * `OrgScopedPermissions` provider always wraps its descendants. The\n * previous shape mounted this on a pathless parent and relied on child\n * routes to declare the param, which could silently fall through to\n * tenant-scoped permissions if a future refactor moved the layout above\n * a tree that doesn't own the segment.\n */\nfunction OrgScopedRouteLayout({ paramName }: { paramName: 'id' | 'organizationId' }) {\n const params = useParams<Record<string, string | undefined>>();\n const orgId = params[paramName];\n if (!orgId) return <Outlet />;\n return (\n <OrgScopedPermissions organizationId={orgId}>\n <Outlet />\n </OrgScopedPermissions>\n );\n}\n\n/**\n * Loading fallback for lazy-loaded pages\n */\nfunction PageLoadingFallback() {\n return (\n <div className=\"flex items-center justify-center min-h-[200px]\">\n <div className=\"size-8 animate-spin rounded-full border-2 border-[var(--color-border-default)] border-t-[var(--color-action-primary-bg)]\" />\n </div>\n );\n}\n\n/**\n * UserOrganizationRoutes\n *\n * User-facing routing surface for the organization MFE, mounted by product\n * application shells (vibecontrols, etc.). Exposes the same shape as the\n * admin routing surface but the list page renders membership-scoped via\n * `mode=\"my\"` (calls `myOrganizations` with `@rbac(scopeType: \"self\")`).\n *\n * Per-action authorisation lives inside each page (`hasPermission` checks\n * for create / update / delete), so users with elevated permissions on an\n * org still see those controls; users without them only see view actions.\n *\n * All routes are unconditionally mounted. Feature flags\n * (`organization.create`, `organization.workspaces`, `organization.members`,\n * `organization.invite`) are applied at the call site — buttons, nav\n * items, section headers inside the pages — not on the route graph. This\n * avoids a class of route/nav mismatches where a page unconditionally\n * navigates to a URL that has been gated off (the previous flag-gated\n * routes produced dead URLs from `OrganizationDetailPage`'s workspace\n * cards, \"Create Workspace\" CTA, \"Manage Members\" button, etc.).\n *\n * Access control:\n * - Per-action `hasPermission` checks inside each page hide controls the\n * user can't use.\n * - Backend resolvers (`organization`, `organizationMembers`, etc.)\n * enforce membership server-side via `userHasAccessToOrganization`, so\n * deep-linking to a URL the user has no access to 403s at the resolver.\n *\n * Lazy loading strategy:\n * - OrganizationsListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better initial bundle size\n */\nexport function UserOrganizationRoutes() {\n return (\n <Suspense fallback={<PageLoadingFallback />}>\n <Routes>\n {/* Organizations — index is the membership-scoped list (eager loaded) */}\n <Route index element={<OrganizationsListPage mode=\"my\" />} />\n <Route path=\"new\" element={<OrganizationCreatePage />} />\n\n {/* Per-org routes — the layout OWNS the `:id` segment so\n `useParams().id` is guaranteed to be set inside\n OrgScopedPermissions and per-page hasPermission() resolves at\n the org's scope, not the outer tenant scope. */}\n <Route path=\":id\" element={<OrgScopedRouteLayout paramName=\"id\" />}>\n <Route index element={<OrganizationDetailPage />} />\n <Route path=\"edit\" element={<OrganizationEditPage />} />\n </Route>\n\n {/* My Invites — user-level, not org-scoped. Declared before the\n `:organizationId` block so the literal \"invites\" wins the rank\n match against the dynamic segment. */}\n <Route path=\"invites\" element={<MyInvitesPage />} />\n\n {/* Workspaces — `OrganizationDetailPage` navigates here on workspace\n card click and the \"Create Workspace\" CTA. Declared with literal\n \"workspaces\" prefix so it ranks above the bare `:organizationId`\n dynamic-segment routes below. */}\n <Route path=\"workspaces\" element={<WorkspacesListPage />} />\n <Route path=\"workspaces/new\" element={<WorkspaceCreatePage />} />\n <Route path=\"workspaces/:id\" element={<WorkspaceDetailPage />} />\n <Route path=\"workspaces/:id/edit\" element={<WorkspaceEditPage />} />\n\n {/* Members — layout OWNS the `:organizationId` segment for the\n same reason as the detail block above. The inner pages already\n read `useParams().organizationId`, so we keep the segment name\n distinct from `:id` even though both resolve to an org id. */}\n <Route path=\":organizationId\" element={<OrgScopedRouteLayout paramName=\"organizationId\" />}>\n <Route path=\"members/invite\" element={<InviteMemberPage />} />\n <Route path=\"members\" element={<MembersListPage />} />\n <Route path=\"members/:userId\" element={<MemberDetailPage />} />\n </Route>\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"UserOrganizationRoutes.js","names":[],"sources":["../../src/organization-management/UserOrganizationRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Outlet, Route, Routes, useParams } from 'react-router-dom';\n\n// Eager load — main entry point page\nimport { OrganizationsListPage } from './modules/organizations/pages/OrganizationsListPage';\nimport { OrgScopedPermissions } from './providers/OrgScopedPermissions';\n\n// Lazy load — secondary pages. Same set as the admin surface but the list\n// page below renders in `mode=\"my\"` (membership-scoped), and per-action\n// permission gates inside each page (hasPermission(PERMISSIONS.ORG_*))\n// already hide buttons the user can't use.\nconst OrganizationCreatePage = lazy(() =>\n import('./modules/organizations/pages/OrganizationCreatePage').then((m) => ({\n default: m.OrganizationCreatePage,\n }))\n);\nconst OrganizationDetailPage = lazy(() =>\n import('./modules/organizations/pages/OrganizationDetailPage').then((m) => ({\n default: m.OrganizationDetailPage,\n }))\n);\nconst OrganizationEditPage = lazy(() =>\n import('./modules/organizations/pages/OrganizationEditPage').then((m) => ({\n default: m.OrganizationEditPage,\n }))\n);\nconst WorkspacesListPage = lazy(() =>\n import('./modules/workspaces/pages/WorkspacesListPage').then((m) => ({\n default: m.WorkspacesListPage,\n }))\n);\nconst WorkspaceCreatePage = lazy(() =>\n import('./modules/workspaces/pages/WorkspaceCreatePage').then((m) => ({\n default: m.WorkspaceCreatePage,\n }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./modules/workspaces/pages/WorkspaceDetailPage').then((m) => ({\n default: m.WorkspaceDetailPage,\n }))\n);\nconst WorkspaceEditPage = lazy(() =>\n import('./modules/workspaces/pages/WorkspaceEditPage').then((m) => ({\n default: m.WorkspaceEditPage,\n }))\n);\nconst MembersListPage = lazy(() =>\n import('./modules/members/pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst MemberDetailPage = lazy(() =>\n import('./modules/members/pages/MemberDetailPage').then((m) => ({ default: m.MemberDetailPage }))\n);\nconst InviteMemberPage = lazy(() =>\n import('./modules/members/pages/InviteMemberPage').then((m) => ({ default: m.InviteMemberPage }))\n);\nconst MyInvitesPage = lazy(() =>\n import('./modules/members/pages/MyInvitesPage').then((m) => ({ default: m.MyInvitesPage }))\n);\nconst InvitePeoplePage = lazy(() =>\n import('./modules/members/pages/InvitePeoplePage').then((m) => ({\n default: m.InvitePeoplePage,\n }))\n);\n\n/**\n * Layout wrapper that scopes the PermissionProvider to the current\n * organization's id (read from the URL). This layout owns the `:id` or\n * `:organizationId` URL segment directly (see route declarations below),\n * so `useParams()[paramName]` is guaranteed to be populated and the\n * `OrgScopedPermissions` provider always wraps its descendants. The\n * previous shape mounted this on a pathless parent and relied on child\n * routes to declare the param, which could silently fall through to\n * tenant-scoped permissions if a future refactor moved the layout above\n * a tree that doesn't own the segment.\n */\nfunction OrgScopedRouteLayout({ paramName }: { paramName: 'id' | 'organizationId' }) {\n const params = useParams<Record<string, string | undefined>>();\n const orgId = params[paramName];\n if (!orgId) return <Outlet />;\n return (\n <OrgScopedPermissions organizationId={orgId}>\n <Outlet />\n </OrgScopedPermissions>\n );\n}\n\n/**\n * Loading fallback for lazy-loaded pages\n */\nfunction PageLoadingFallback() {\n return (\n <div className=\"flex items-center justify-center min-h-[200px]\">\n <div className=\"size-8 animate-spin rounded-full border-2 border-[var(--color-border-default)] border-t-[var(--color-action-primary-bg)]\" />\n </div>\n );\n}\n\n/**\n * UserOrganizationRoutes\n *\n * User-facing routing surface for the organization MFE, mounted by product\n * application shells (vibecontrols, etc.). Exposes the same shape as the\n * admin routing surface but the list page renders membership-scoped via\n * `mode=\"my\"` (calls `myOrganizations` with `@rbac(scopeType: \"self\")`).\n *\n * Per-action authorisation lives inside each page (`hasPermission` checks\n * for create / update / delete), so users with elevated permissions on an\n * org still see those controls; users without them only see view actions.\n *\n * All routes are unconditionally mounted. Feature flags\n * (`organization.create`, `organization.workspaces`, `organization.members`,\n * `organization.invite`) are applied at the call site — buttons, nav\n * items, section headers inside the pages — not on the route graph. This\n * avoids a class of route/nav mismatches where a page unconditionally\n * navigates to a URL that has been gated off (the previous flag-gated\n * routes produced dead URLs from `OrganizationDetailPage`'s workspace\n * cards, \"Create Workspace\" CTA, \"Manage Members\" button, etc.).\n *\n * Access control:\n * - Per-action `hasPermission` checks inside each page hide controls the\n * user can't use.\n * - Backend resolvers (`organization`, `organizationMembers`, etc.)\n * enforce membership server-side via `userHasAccessToOrganization`, so\n * deep-linking to a URL the user has no access to 403s at the resolver.\n *\n * Lazy loading strategy:\n * - OrganizationsListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better initial bundle size\n */\nexport function UserOrganizationRoutes() {\n return (\n <Suspense fallback={<PageLoadingFallback />}>\n <Routes>\n {/* Organizations — index is the membership-scoped list (eager loaded) */}\n <Route index element={<OrganizationsListPage mode=\"my\" />} />\n <Route path=\"new\" element={<OrganizationCreatePage />} />\n\n {/* Per-org routes — the layout OWNS the `:id` segment so\n `useParams().id` is guaranteed to be set inside\n OrgScopedPermissions and per-page hasPermission() resolves at\n the org's scope, not the outer tenant scope. */}\n <Route path=\":id\" element={<OrgScopedRouteLayout paramName=\"id\" />}>\n <Route index element={<OrganizationDetailPage />} />\n <Route path=\"edit\" element={<OrganizationEditPage />} />\n </Route>\n\n {/* My Invites — user-level, not org-scoped. Declared before the\n `:organizationId` block so the literal \"invites\" wins the rank\n match against the dynamic segment. */}\n <Route path=\"invites\" element={<MyInvitesPage />} />\n\n {/* Invite People — globally discoverable chooser. Lists orgs the\n user can invite into, then deep-links to per-org invite page. */}\n <Route path=\"invite\" element={<InvitePeoplePage />} />\n\n {/* Workspaces — `OrganizationDetailPage` navigates here on workspace\n card click and the \"Create Workspace\" CTA. Declared with literal\n \"workspaces\" prefix so it ranks above the bare `:organizationId`\n dynamic-segment routes below. */}\n <Route path=\"workspaces\" element={<WorkspacesListPage />} />\n <Route path=\"workspaces/new\" element={<WorkspaceCreatePage />} />\n <Route path=\"workspaces/:id\" element={<WorkspaceDetailPage />} />\n <Route path=\"workspaces/:id/edit\" element={<WorkspaceEditPage />} />\n\n {/* Members — layout OWNS the `:organizationId` segment for the\n same reason as the detail block above. The inner pages already\n read `useParams().organizationId`, so we keep the segment name\n distinct from `:id` even though both resolve to an org id. */}\n <Route path=\":organizationId\" element={<OrgScopedRouteLayout paramName=\"organizationId\" />}>\n <Route path=\"members/invite\" element={<InviteMemberPage />} />\n <Route path=\"members\" element={<MembersListPage />} />\n <Route path=\"members/:userId\" element={<MemberDetailPage />} />\n </Route>\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,291 @@
1
+ import { toast as e } from "../../../../node_modules/sonner/dist/index.js";
2
+ import { useTr as t } from "../../../shared/hooks/useTr.js";
3
+ import { downloadTemplate as n, formatInviteError as r, parseBulkInviteCsv as i } from "../utils/bulkInviteCsv.js";
4
+ import { useCallback as a, useMemo as o, useRef as s, useState as c } from "react";
5
+ import { CheckCircle2 as l, Download as u, Loader2 as d, Send as f, Upload as p, XCircle as m } from "lucide-react";
6
+ import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
7
+ //#region src/organization-management/modules/members/components/BulkInviteForm.tsx
8
+ function v({ organizationId: v, inviteMember: y, onCancel: b, loading: x }) {
9
+ let S = t(), C = s(null), [w, T] = c([]), [E, D] = c(null), [O, k] = c(!1), [A, j] = c(!1), M = a((t) => {
10
+ let n = new FileReader();
11
+ n.onload = () => {
12
+ T(i(typeof n.result == "string" ? n.result : "").map((e) => ({
13
+ ...e,
14
+ status: { kind: "idle" }
15
+ }))), D(t.name);
16
+ }, n.onerror = () => {
17
+ e.error(S("members.bulkInvite.readError", "Failed to read CSV file."));
18
+ }, n.readAsText(t);
19
+ }, [S]), N = (e) => {
20
+ let t = e.target.files?.[0];
21
+ t && M(t), e.target.value = "";
22
+ }, P = (e) => {
23
+ e.preventDefault(), j(!1);
24
+ let t = e.dataTransfer.files?.[0];
25
+ t && M(t);
26
+ }, F = o(() => ({
27
+ total: w.length,
28
+ withErrors: w.filter((e) => e.errors.length > 0).length,
29
+ sendable: w.filter((e) => e.errors.length === 0).length,
30
+ invited: w.filter((e) => e.status.kind === "invited").length,
31
+ failed: w.filter((e) => e.status.kind === "failed").length
32
+ }), [w]), I = async () => {
33
+ if (F.sendable === 0 || O) return;
34
+ k(!0);
35
+ let t = w.map((e, t) => ({
36
+ r: e,
37
+ idx: t
38
+ })).filter(({ r: e }) => e.errors.length === 0 && e.status.kind !== "invited");
39
+ for (let { idx: e } of t) {
40
+ T((t) => t.map((t, n) => n === e ? {
41
+ ...t,
42
+ status: { kind: "sending" }
43
+ } : t));
44
+ let t = w[e];
45
+ try {
46
+ await y({
47
+ organizationId: v,
48
+ email: t.email,
49
+ message: t.message
50
+ }), T((t) => t.map((t, n) => n === e ? {
51
+ ...t,
52
+ status: { kind: "invited" }
53
+ } : t));
54
+ } catch (t) {
55
+ let n = r(t, S);
56
+ T((t) => t.map((t, r) => r === e ? {
57
+ ...t,
58
+ status: {
59
+ kind: "failed",
60
+ message: n
61
+ }
62
+ } : t));
63
+ }
64
+ }
65
+ k(!1), T((t) => {
66
+ let n = t.filter((e) => e.status.kind === "invited").length, r = t.filter((e) => e.status.kind === "failed").length;
67
+ return e.success(`${S("members.bulkInvite.summaryInvited", "Invited")} ${n} ${S("members.bulkInvite.summaryOf", "of")} ${t.length}. ${r} ${S("members.bulkInvite.summaryFailed", "failed")}.`), t;
68
+ });
69
+ }, L = w.length > 0, R = F.sendable > 0 && !O && !x;
70
+ return /* @__PURE__ */ _("div", {
71
+ className: "space-y-5",
72
+ children: [
73
+ /* @__PURE__ */ _("div", {
74
+ className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
75
+ children: [/* @__PURE__ */ g("p", {
76
+ className: "text-sm text-[var(--color-text-muted)]",
77
+ children: S("members.bulkInvite.description", "Upload a CSV with one row per person. Required column: email. Optional: role, message.")
78
+ }), /* @__PURE__ */ _("button", {
79
+ type: "button",
80
+ onClick: n,
81
+ className: "inline-flex min-h-10 items-center justify-center gap-2 rounded-lg border border-[var(--color-border-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-bg-subtle)]",
82
+ children: [/* @__PURE__ */ g(u, {
83
+ size: 16,
84
+ "aria-hidden": "true"
85
+ }), S("members.bulkInvite.downloadTemplate", "Download template")]
86
+ })]
87
+ }),
88
+ /* @__PURE__ */ _("div", {
89
+ onDragOver: (e) => {
90
+ e.preventDefault(), j(!0);
91
+ },
92
+ onDragLeave: () => j(!1),
93
+ onDrop: P,
94
+ className: `flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed p-8 transition-colors ${A ? "border-[var(--color-action-primary-bg)] bg-[var(--color-status-info-bgSubtle)]" : "border-[var(--color-border-default)] bg-[var(--color-bg-canvas)]"}`,
95
+ children: [
96
+ /* @__PURE__ */ g(p, {
97
+ size: 28,
98
+ className: "text-[var(--color-text-secondary)]",
99
+ "aria-hidden": "true"
100
+ }),
101
+ /* @__PURE__ */ _("div", {
102
+ className: "text-center",
103
+ children: [/* @__PURE__ */ g("p", {
104
+ className: "text-sm font-medium text-[var(--color-text-primary)]",
105
+ children: E || S("members.bulkInvite.dropHere", "Drop a CSV file here or click to browse")
106
+ }), /* @__PURE__ */ g("p", {
107
+ className: "mt-1 text-xs text-[var(--color-text-muted)]",
108
+ children: S("members.bulkInvite.csvOnly", "CSV files only. Max 1000 rows recommended.")
109
+ })]
110
+ }),
111
+ /* @__PURE__ */ g("input", {
112
+ ref: C,
113
+ type: "file",
114
+ accept: ".csv,text/csv",
115
+ onChange: N,
116
+ className: "hidden",
117
+ "aria-label": S("members.bulkInvite.fileInputLabel", "Choose CSV file")
118
+ }),
119
+ /* @__PURE__ */ g("button", {
120
+ type: "button",
121
+ onClick: () => C.current?.click(),
122
+ className: "inline-flex min-h-10 items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-4 py-2 text-sm text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90",
123
+ children: S("members.bulkInvite.chooseFile", "Choose file")
124
+ })
125
+ ]
126
+ }),
127
+ L && /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ _("div", {
128
+ className: "flex flex-wrap gap-4 text-xs text-[var(--color-text-secondary)]",
129
+ children: [
130
+ /* @__PURE__ */ _("span", { children: [
131
+ S("members.bulkInvite.statsTotal", "Total rows"),
132
+ ": ",
133
+ F.total
134
+ ] }),
135
+ /* @__PURE__ */ _("span", { children: [
136
+ S("members.bulkInvite.statsValid", "Ready"),
137
+ ": ",
138
+ F.sendable
139
+ ] }),
140
+ F.withErrors > 0 && /* @__PURE__ */ _("span", {
141
+ className: "text-[var(--color-status-error-text)]",
142
+ children: [
143
+ S("members.bulkInvite.statsErrors", "With errors"),
144
+ ": ",
145
+ F.withErrors
146
+ ]
147
+ }),
148
+ F.invited > 0 && /* @__PURE__ */ _("span", {
149
+ className: "text-[var(--color-status-success-text)]",
150
+ children: [
151
+ S("members.bulkInvite.statsInvited", "Invited"),
152
+ ": ",
153
+ F.invited
154
+ ]
155
+ }),
156
+ F.failed > 0 && /* @__PURE__ */ _("span", {
157
+ className: "text-[var(--color-status-error-text)]",
158
+ children: [
159
+ S("members.bulkInvite.statsFailed", "Failed"),
160
+ ": ",
161
+ F.failed
162
+ ]
163
+ })
164
+ ]
165
+ }), /* @__PURE__ */ g("div", {
166
+ className: "overflow-hidden rounded-xl border border-[var(--color-border-default)]",
167
+ children: /* @__PURE__ */ g("div", {
168
+ className: "overflow-x-auto",
169
+ children: /* @__PURE__ */ _("table", {
170
+ className: "min-w-full",
171
+ children: [/* @__PURE__ */ g("thead", {
172
+ className: "bg-[var(--color-bg-subtle)]",
173
+ children: /* @__PURE__ */ _("tr", { children: [
174
+ /* @__PURE__ */ g("th", {
175
+ className: "px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]",
176
+ children: "#"
177
+ }),
178
+ /* @__PURE__ */ g("th", {
179
+ className: "px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]",
180
+ children: S("members.bulkInvite.colEmail", "Email")
181
+ }),
182
+ /* @__PURE__ */ g("th", {
183
+ className: "px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]",
184
+ children: S("members.bulkInvite.colRole", "Role")
185
+ }),
186
+ /* @__PURE__ */ g("th", {
187
+ className: "px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]",
188
+ children: S("members.bulkInvite.colStatus", "Status")
189
+ })
190
+ ] })
191
+ }), /* @__PURE__ */ g("tbody", {
192
+ className: "divide-y divide-[var(--color-border-default)]",
193
+ children: w.map((e) => {
194
+ let t = e.errors.length > 0;
195
+ return /* @__PURE__ */ _("tr", {
196
+ className: "bg-[var(--color-bg-surface)]",
197
+ children: [
198
+ /* @__PURE__ */ g("td", {
199
+ className: "px-3 py-2 text-xs text-[var(--color-text-secondary)]",
200
+ children: e.rowNumber
201
+ }),
202
+ /* @__PURE__ */ g("td", {
203
+ className: "px-3 py-2 text-sm text-[var(--color-text-primary)]",
204
+ children: e.email || /* @__PURE__ */ g("span", {
205
+ className: "italic text-[var(--color-text-muted)]",
206
+ children: S("members.bulkInvite.empty", "(empty)")
207
+ })
208
+ }),
209
+ /* @__PURE__ */ g("td", {
210
+ className: "px-3 py-2 text-xs text-[var(--color-text-secondary)]",
211
+ children: e.role || /* @__PURE__ */ g("span", {
212
+ className: "text-[var(--color-text-muted)]",
213
+ children: "MEMBER"
214
+ })
215
+ }),
216
+ /* @__PURE__ */ g("td", {
217
+ className: "px-3 py-2 text-xs",
218
+ children: t ? /* @__PURE__ */ _("span", {
219
+ className: "inline-flex items-center gap-1 text-[var(--color-status-error-text)]",
220
+ children: [/* @__PURE__ */ g(m, {
221
+ size: 14,
222
+ "aria-hidden": "true"
223
+ }), e.errors.join(" ")]
224
+ }) : e.status.kind === "sending" ? /* @__PURE__ */ _("span", {
225
+ className: "inline-flex items-center gap-1 text-[var(--color-text-secondary)]",
226
+ children: [/* @__PURE__ */ g(d, {
227
+ size: 14,
228
+ className: "animate-spin",
229
+ "aria-hidden": "true"
230
+ }), S("members.bulkInvite.statusSending", "Sending...")]
231
+ }) : e.status.kind === "invited" ? /* @__PURE__ */ _("span", {
232
+ className: "inline-flex items-center gap-1 text-[var(--color-status-success-text)]",
233
+ children: [/* @__PURE__ */ g(l, {
234
+ size: 14,
235
+ "aria-hidden": "true"
236
+ }), S("members.bulkInvite.statusInvited", "Invited")]
237
+ }) : e.status.kind === "failed" ? /* @__PURE__ */ _("span", {
238
+ className: "inline-flex items-center gap-1 text-[var(--color-status-error-text)]",
239
+ children: [/* @__PURE__ */ g(m, {
240
+ size: 14,
241
+ "aria-hidden": "true"
242
+ }), e.status.message]
243
+ }) : /* @__PURE__ */ g("span", {
244
+ className: "text-[var(--color-text-secondary)]",
245
+ children: S("members.bulkInvite.statusReady", "Ready")
246
+ })
247
+ })
248
+ ]
249
+ }, `${e.rowNumber}-${e.email}`);
250
+ })
251
+ })]
252
+ })
253
+ })
254
+ })] }),
255
+ /* @__PURE__ */ _("div", {
256
+ className: "flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row",
257
+ children: [/* @__PURE__ */ _("button", {
258
+ type: "button",
259
+ onClick: I,
260
+ disabled: !R,
261
+ className: "inline-flex items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-6 py-2.5 font-medium text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90 hover:scale-[1.02] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[200px]",
262
+ children: [
263
+ O ? /* @__PURE__ */ g(d, {
264
+ size: 16,
265
+ className: "animate-spin",
266
+ "aria-hidden": "true"
267
+ }) : /* @__PURE__ */ g(f, {
268
+ size: 16,
269
+ "aria-hidden": "true"
270
+ }),
271
+ S("members.bulkInvite.send", "Send"),
272
+ " ",
273
+ F.sendable,
274
+ " ",
275
+ S("members.bulkInvite.sendSuffix", "invites")
276
+ ]
277
+ }), /* @__PURE__ */ g("button", {
278
+ type: "button",
279
+ onClick: b,
280
+ disabled: O,
281
+ className: "rounded-lg border border-[var(--color-border-default)] bg-transparent px-6 py-2.5 font-medium text-[var(--color-text-primary)] transition-all duration-200 hover:bg-[var(--color-bg-subtle)] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[140px]",
282
+ children: S("common.cancel", "Cancel")
283
+ })]
284
+ })
285
+ ]
286
+ });
287
+ }
288
+ //#endregion
289
+ export { v as BulkInviteForm };
290
+
291
+ //# sourceMappingURL=BulkInviteForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BulkInviteForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/BulkInviteForm.tsx"],"sourcesContent":["import { useCallback, useMemo, useRef, useState } from 'react';\nimport { CheckCircle2, Download, Loader2, Send, Upload, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport {\n type BulkInviteRow,\n downloadTemplate,\n formatInviteError,\n parseBulkInviteCsv,\n} from '../utils/bulkInviteCsv';\n\ninterface BulkInviteFormProps {\n organizationId: string;\n inviteMember: (input: {\n organizationId: string;\n email: string;\n message?: string;\n }) => Promise<unknown>;\n onCancel: () => void;\n loading?: boolean;\n}\n\ntype RowStatus =\n | { kind: 'idle' }\n | { kind: 'sending' }\n | { kind: 'invited' }\n | { kind: 'failed'; message: string };\n\ninterface PreviewRow extends BulkInviteRow {\n status: RowStatus;\n}\n\n/**\n * Bulk-invite via CSV upload. Sequentially fires the per-row invite\n * mutation (concurrency 1) so we don't hammer the gateway and so per-row\n * errors are deterministic. Reuses the same mutation as single-invite,\n * which keeps backend behaviour (refetch, OTel events) identical.\n */\nexport function BulkInviteForm({\n organizationId,\n inviteMember,\n onCancel,\n loading,\n}: BulkInviteFormProps) {\n const tr = useTr();\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [rows, setRows] = useState<PreviewRow[]>([]);\n const [fileName, setFileName] = useState<string | null>(null);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [dragActive, setDragActive] = useState(false);\n\n const handleFile = useCallback(\n (file: File) => {\n const reader = new FileReader();\n reader.onload = () => {\n const text = typeof reader.result === 'string' ? reader.result : '';\n const parsed = parseBulkInviteCsv(text);\n setRows(parsed.map((r) => ({ ...r, status: { kind: 'idle' } })));\n setFileName(file.name);\n };\n reader.onerror = () => {\n toast.error(tr('members.bulkInvite.readError', 'Failed to read CSV file.'));\n };\n reader.readAsText(file);\n },\n [tr]\n );\n\n const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const file = e.target.files?.[0];\n if (file) handleFile(file);\n // Reset so re-uploading the same file fires onChange again\n e.target.value = '';\n };\n\n const onDrop = (e: React.DragEvent<HTMLDivElement>) => {\n e.preventDefault();\n setDragActive(false);\n const file = e.dataTransfer.files?.[0];\n if (file) handleFile(file);\n };\n\n const summary = useMemo(() => {\n const total = rows.length;\n const withErrors = rows.filter((r) => r.errors.length > 0).length;\n const sendable = rows.filter((r) => r.errors.length === 0).length;\n const invited = rows.filter((r) => r.status.kind === 'invited').length;\n const failed = rows.filter((r) => r.status.kind === 'failed').length;\n return { total, withErrors, sendable, invited, failed };\n }, [rows]);\n\n const handleSend = async () => {\n if (summary.sendable === 0 || isSubmitting) return;\n setIsSubmitting(true);\n\n const sendableIndices = rows\n .map((r, idx) => ({ r, idx }))\n .filter(({ r }) => r.errors.length === 0 && r.status.kind !== 'invited');\n\n for (const { idx } of sendableIndices) {\n setRows((prev) =>\n prev.map((row, i) => (i === idx ? { ...row, status: { kind: 'sending' } } : row))\n );\n const row = rows[idx];\n try {\n await inviteMember({\n organizationId,\n email: row.email,\n message: row.message,\n });\n setRows((prev) =>\n prev.map((r, i) => (i === idx ? { ...r, status: { kind: 'invited' } } : r))\n );\n } catch (err) {\n const message = formatInviteError(err, tr);\n setRows((prev) =>\n prev.map((r, i) => (i === idx ? { ...r, status: { kind: 'failed', message } } : r))\n );\n }\n }\n\n setIsSubmitting(false);\n setRows((prev) => {\n const invited = prev.filter((r) => r.status.kind === 'invited').length;\n const failed = prev.filter((r) => r.status.kind === 'failed').length;\n toast.success(\n `${tr('members.bulkInvite.summaryInvited', 'Invited')} ${invited} ${tr('members.bulkInvite.summaryOf', 'of')} ${prev.length}. ${failed} ${tr('members.bulkInvite.summaryFailed', 'failed')}.`\n );\n return prev;\n });\n };\n\n const hasRows = rows.length > 0;\n const canSubmit = summary.sendable > 0 && !isSubmitting && !loading;\n\n return (\n <div className=\"space-y-5\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-[var(--color-text-muted)]\">\n {tr(\n 'members.bulkInvite.description',\n 'Upload a CSV with one row per person. Required column: email. Optional: role, message.'\n )}\n </p>\n <button\n type=\"button\"\n onClick={downloadTemplate}\n className=\"inline-flex min-h-10 items-center justify-center gap-2 rounded-lg border border-[var(--color-border-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] transition-colors hover:bg-[var(--color-bg-subtle)]\"\n >\n <Download size={16} aria-hidden=\"true\" />\n {tr('members.bulkInvite.downloadTemplate', 'Download template')}\n </button>\n </div>\n\n <div\n onDragOver={(e) => {\n e.preventDefault();\n setDragActive(true);\n }}\n onDragLeave={() => setDragActive(false)}\n onDrop={onDrop}\n className={`flex flex-col items-center justify-center gap-3 rounded-xl border-2 border-dashed p-8 transition-colors ${\n dragActive\n ? 'border-[var(--color-action-primary-bg)] bg-[var(--color-status-info-bgSubtle)]'\n : 'border-[var(--color-border-default)] bg-[var(--color-bg-canvas)]'\n }`}\n >\n <Upload size={28} className=\"text-[var(--color-text-secondary)]\" aria-hidden=\"true\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium text-[var(--color-text-primary)]\">\n {fileName\n ? fileName\n : tr('members.bulkInvite.dropHere', 'Drop a CSV file here or click to browse')}\n </p>\n <p className=\"mt-1 text-xs text-[var(--color-text-muted)]\">\n {tr('members.bulkInvite.csvOnly', 'CSV files only. Max 1000 rows recommended.')}\n </p>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,text/csv\"\n onChange={onFileChange}\n className=\"hidden\"\n aria-label={tr('members.bulkInvite.fileInputLabel', 'Choose CSV file')}\n />\n <button\n type=\"button\"\n onClick={() => fileInputRef.current?.click()}\n className=\"inline-flex min-h-10 items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-4 py-2 text-sm text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90\"\n >\n {tr('members.bulkInvite.chooseFile', 'Choose file')}\n </button>\n </div>\n\n {hasRows && (\n <>\n <div className=\"flex flex-wrap gap-4 text-xs text-[var(--color-text-secondary)]\">\n <span>\n {tr('members.bulkInvite.statsTotal', 'Total rows')}: {summary.total}\n </span>\n <span>\n {tr('members.bulkInvite.statsValid', 'Ready')}: {summary.sendable}\n </span>\n {summary.withErrors > 0 && (\n <span className=\"text-[var(--color-status-error-text)]\">\n {tr('members.bulkInvite.statsErrors', 'With errors')}: {summary.withErrors}\n </span>\n )}\n {summary.invited > 0 && (\n <span className=\"text-[var(--color-status-success-text)]\">\n {tr('members.bulkInvite.statsInvited', 'Invited')}: {summary.invited}\n </span>\n )}\n {summary.failed > 0 && (\n <span className=\"text-[var(--color-status-error-text)]\">\n {tr('members.bulkInvite.statsFailed', 'Failed')}: {summary.failed}\n </span>\n )}\n </div>\n\n <div className=\"overflow-hidden rounded-xl border border-[var(--color-border-default)]\">\n <div className=\"overflow-x-auto\">\n <table className=\"min-w-full\">\n <thead className=\"bg-[var(--color-bg-subtle)]\">\n <tr>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n #\n </th>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.colEmail', 'Email')}\n </th>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.colRole', 'Role')}\n </th>\n <th className=\"px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.colStatus', 'Status')}\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-[var(--color-border-default)]\">\n {rows.map((row) => {\n const hasParseErrors = row.errors.length > 0;\n return (\n <tr\n key={`${row.rowNumber}-${row.email}`}\n className=\"bg-[var(--color-bg-surface)]\"\n >\n <td className=\"px-3 py-2 text-xs text-[var(--color-text-secondary)]\">\n {row.rowNumber}\n </td>\n <td className=\"px-3 py-2 text-sm text-[var(--color-text-primary)]\">\n {row.email || (\n <span className=\"italic text-[var(--color-text-muted)]\">\n {tr('members.bulkInvite.empty', '(empty)')}\n </span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs text-[var(--color-text-secondary)]\">\n {row.role || (\n <span className=\"text-[var(--color-text-muted)]\">MEMBER</span>\n )}\n </td>\n <td className=\"px-3 py-2 text-xs\">\n {hasParseErrors ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-error-text)]\">\n <XCircle size={14} aria-hidden=\"true\" />\n {row.errors.join(' ')}\n </span>\n ) : row.status.kind === 'sending' ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-text-secondary)]\">\n <Loader2 size={14} className=\"animate-spin\" aria-hidden=\"true\" />\n {tr('members.bulkInvite.statusSending', 'Sending...')}\n </span>\n ) : row.status.kind === 'invited' ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-success-text)]\">\n <CheckCircle2 size={14} aria-hidden=\"true\" />\n {tr('members.bulkInvite.statusInvited', 'Invited')}\n </span>\n ) : row.status.kind === 'failed' ? (\n <span className=\"inline-flex items-center gap-1 text-[var(--color-status-error-text)]\">\n <XCircle size={14} aria-hidden=\"true\" />\n {row.status.message}\n </span>\n ) : (\n <span className=\"text-[var(--color-text-secondary)]\">\n {tr('members.bulkInvite.statusReady', 'Ready')}\n </span>\n )}\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </div>\n </>\n )}\n\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"button\"\n onClick={handleSend}\n disabled={!canSubmit}\n className=\"inline-flex items-center justify-center gap-2 rounded-lg bg-[var(--color-action-primary-bg)] px-6 py-2.5 font-medium text-[var(--color-action-primary-text)] transition-all duration-200 hover:opacity-90 hover:scale-[1.02] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[200px]\"\n >\n {isSubmitting ? (\n <Loader2 size={16} className=\"animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Send size={16} aria-hidden=\"true\" />\n )}\n {tr('members.bulkInvite.send', 'Send')} {summary.sendable}{' '}\n {tr('members.bulkInvite.sendSuffix', 'invites')}\n </button>\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={isSubmitting}\n className=\"rounded-lg border border-[var(--color-border-default)] bg-transparent px-6 py-2.5 font-medium text-[var(--color-text-primary)] transition-all duration-200 hover:bg-[var(--color-bg-subtle)] active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[140px]\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}