@burdenoff/microfe-organization 2026.608.1 → 2026.624.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/generated/global-operations.js +78 -63
  2. package/dist/generated/global-operations.js.map +1 -1
  3. package/dist/generated/global-types.js.map +1 -1
  4. package/dist/organization-management/UserOrganizationRoutes.js +13 -9
  5. package/dist/organization-management/UserOrganizationRoutes.js.map +1 -1
  6. package/dist/organization-management/modules/members/components/BulkInviteForm.js +2 -2
  7. package/dist/organization-management/modules/members/components/BulkInviteForm.js.map +1 -1
  8. package/dist/organization-management/modules/members/components/InviteMemberForm.js +2 -2
  9. package/dist/organization-management/modules/members/components/InviteMemberForm.js.map +1 -1
  10. package/dist/organization-management/modules/members/components/MyInviteCard.js +2 -2
  11. package/dist/organization-management/modules/members/components/MyInviteCard.js.map +1 -1
  12. package/dist/organization-management/modules/members/hooks/useInvites.js +79 -65
  13. package/dist/organization-management/modules/members/hooks/useInvites.js.map +1 -1
  14. package/dist/organization-management/modules/members/pages/MemberDetailPage.js +1 -1
  15. package/dist/organization-management/modules/members/pages/MemberDetailPage.js.map +1 -1
  16. package/dist/organization-management/modules/members/pages/MembersListPage.js +2 -2
  17. package/dist/organization-management/modules/members/pages/MembersListPage.js.map +1 -1
  18. package/dist/organization-management/modules/members/pages/MyInvitesPage.js +2 -2
  19. package/dist/organization-management/modules/members/pages/MyInvitesPage.js.map +1 -1
  20. package/dist/organization-management/modules/members/pages/OrganizationInviteAcceptancePage.js +287 -0
  21. package/dist/organization-management/modules/members/pages/OrganizationInviteAcceptancePage.js.map +1 -0
  22. package/dist/organization-management/modules/organizations/components/OrganizationForm.js +114 -114
  23. package/dist/organization-management/modules/organizations/components/OrganizationForm.js.map +1 -1
  24. package/dist/organization-management/modules/organizations/pages/OrganizationCreatePage.js +9 -8
  25. package/dist/organization-management/modules/organizations/pages/OrganizationCreatePage.js.map +1 -1
  26. package/dist/organization-management/modules/organizations/pages/OrganizationDetailPage.js +3 -3
  27. package/dist/organization-management/modules/organizations/pages/OrganizationDetailPage.js.map +1 -1
  28. package/dist/organization-management/modules/organizations/pages/OrganizationEditPage.js +1 -1
  29. package/dist/organization-management/modules/organizations/pages/OrganizationEditPage.js.map +1 -1
  30. package/dist/organization-management/modules/organizations/pages/OrganizationsListPage.js +3 -3
  31. package/dist/organization-management/modules/organizations/pages/OrganizationsListPage.js.map +1 -1
  32. package/dist/organization-management/modules/workspaces/components/WorkspaceCard.js +5 -8
  33. package/dist/organization-management/modules/workspaces/components/WorkspaceCard.js.map +1 -1
  34. package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js +88 -136
  35. package/dist/organization-management/modules/workspaces/components/WorkspaceForm.js.map +1 -1
  36. package/dist/organization-management/modules/workspaces/hooks/useWorkspaces.js.map +1 -1
  37. package/dist/organization-management/modules/workspaces/pages/WorkspaceCreatePage.js +1 -1
  38. package/dist/organization-management/modules/workspaces/pages/WorkspaceCreatePage.js.map +1 -1
  39. package/dist/organization-management/modules/workspaces/pages/WorkspaceDetailPage.js +7 -21
  40. package/dist/organization-management/modules/workspaces/pages/WorkspaceDetailPage.js.map +1 -1
  41. package/dist/organization-management/modules/workspaces/pages/WorkspaceEditPage.js +1 -1
  42. package/dist/organization-management/modules/workspaces/pages/WorkspaceEditPage.js.map +1 -1
  43. package/dist/organization-management/modules/workspaces/pages/WorkspacesListPage.js +6 -24
  44. package/dist/organization-management/modules/workspaces/pages/WorkspacesListPage.js.map +1 -1
  45. package/dist/organization-management/shared/components/feedback/EmptyState.js +1 -1
  46. package/dist/organization-management/shared/components/feedback/EmptyState.js.map +1 -1
  47. package/dist/organization-management/shared/components/feedback/ErrorState.js +1 -1
  48. package/dist/organization-management/shared/components/feedback/ErrorState.js.map +1 -1
  49. package/dist/organization-management/shared/components/input/SearchInput.js +1 -1
  50. package/dist/organization-management/shared/components/input/SearchInput.js.map +1 -1
  51. package/package.json +1 -1
@@ -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 }))), x = r(() => import("./modules/members/pages/InvitePeoplePage.js").then((e) => ({ default: e.InvitePeoplePage })));
8
- function S({ 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/OrganizationInviteAcceptancePage.js").then((e) => ({ default: e.OrganizationInviteAcceptancePage }))), S = r(() => import("./modules/members/pages/InvitePeoplePage.js").then((e) => ({ default: e.InvitePeoplePage })));
8
+ function C({ 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 C() {
15
+ function w() {
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 w() {
21
+ function T() {
22
22
  return /* @__PURE__ */ i(n, {
23
- fallback: /* @__PURE__ */ i(C, {}),
23
+ fallback: /* @__PURE__ */ i(w, {}),
24
24
  children: /* @__PURE__ */ a(c, { children: [
25
25
  /* @__PURE__ */ i(s, {
26
26
  index: !0,
@@ -32,7 +32,7 @@ function w() {
32
32
  }),
33
33
  /* @__PURE__ */ a(s, {
34
34
  path: ":id",
35
- element: /* @__PURE__ */ i(S, { paramName: "id" }),
35
+ element: /* @__PURE__ */ i(C, { paramName: "id" }),
36
36
  children: [/* @__PURE__ */ i(s, {
37
37
  index: !0,
38
38
  element: /* @__PURE__ */ i(d, {})
@@ -41,6 +41,10 @@ function w() {
41
41
  element: /* @__PURE__ */ i(f, {})
42
42
  })]
43
43
  }),
44
+ /* @__PURE__ */ i(s, {
45
+ path: "invite/:inviteId",
46
+ element: /* @__PURE__ */ i(x, {})
47
+ }),
44
48
  /* @__PURE__ */ i(s, {
45
49
  path: "invites",
46
50
  element: /* @__PURE__ */ i(b, { activeTab: "workspace" })
@@ -51,7 +55,7 @@ function w() {
51
55
  }),
52
56
  /* @__PURE__ */ i(s, {
53
57
  path: "invite",
54
- element: /* @__PURE__ */ i(x, {})
58
+ element: /* @__PURE__ */ i(S, {})
55
59
  }),
56
60
  /* @__PURE__ */ i(s, {
57
61
  path: "workspaces",
@@ -71,7 +75,7 @@ function w() {
71
75
  }),
72
76
  /* @__PURE__ */ a(s, {
73
77
  path: ":organizationId",
74
- element: /* @__PURE__ */ i(S, { paramName: "organizationId" }),
78
+ element: /* @__PURE__ */ i(C, { paramName: "organizationId" }),
75
79
  children: [
76
80
  /* @__PURE__ */ i(s, {
77
81
  path: "members/invite",
@@ -91,6 +95,6 @@ function w() {
91
95
  });
92
96
  }
93
97
  //#endregion
94
- export { w as UserOrganizationRoutes };
98
+ export { T as UserOrganizationRoutes };
95
99
 
96
100
  //# 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);\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 activeTab=\"workspace\" />} />\n <Route path=\"invites/organizations\" element={<MyInvitesPage activeTab=\"organization\" />} />\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 mode=\"my\" />} />\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 OrganizationInviteAcceptancePage = lazy(() =>\n import('./modules/members/pages/OrganizationInviteAcceptancePage').then((m) => ({\n default: m.OrganizationInviteAcceptancePage,\n }))\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 {/* Per-invite acceptance page reached from the invite email link\n (`/organizations/invite/:inviteId`). Declared before the dynamic\n `:id` / `:organizationId` blocks so the literal \"invite\" segment\n wins the rank match. */}\n <Route path=\"invite/:inviteId\" element={<OrganizationInviteAcceptancePage />} />\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 activeTab=\"workspace\" />} />\n <Route path=\"invites/organizations\" element={<MyInvitesPage activeTab=\"organization\" />} />\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 mode=\"my\" />} />\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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"}
@@ -258,7 +258,7 @@ function v({ organizationId: v, inviteMember: y, onCancel: b, loading: x }) {
258
258
  type: "button",
259
259
  onClick: I,
260
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]",
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 disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[200px]",
262
262
  children: [
263
263
  O ? /* @__PURE__ */ g(d, {
264
264
  size: 16,
@@ -278,7 +278,7 @@ function v({ organizationId: v, inviteMember: y, onCancel: b, loading: x }) {
278
278
  type: "button",
279
279
  onClick: b,
280
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]",
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)] disabled:cursor-not-allowed disabled:opacity-50 sm:min-w-[140px]",
282
282
  children: S("common.cancel", "Cancel")
283
283
  })]
284
284
  })
@@ -1 +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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"}
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 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)] 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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"}
@@ -99,7 +99,7 @@ function s({ organizationId: s, onSubmit: c, onCancel: l, loading: u }) {
99
99
  children: [/* @__PURE__ */ o("button", {
100
100
  type: "submit",
101
101
  disabled: u,
102
- className: "\n inline-flex items-center gap-2 px-6 py-2.5\n bg-[var(--color-action-primary-bg)]\n text-[var(--color-action-primary-text)]\n rounded-lg\n hover:opacity-90 hover:scale-[1.02]\n active:scale-[0.98]\n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[180px]\n ",
102
+ className: "\n inline-flex items-center gap-2 px-6 py-2.5\n bg-[var(--color-action-primary-bg)]\n text-[var(--color-action-primary-text)]\n rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[180px]\n ",
103
103
  children: [u ? /* @__PURE__ */ a(n, {
104
104
  size: 16,
105
105
  className: "animate-spin"
@@ -108,7 +108,7 @@ function s({ organizationId: s, onSubmit: c, onCancel: l, loading: u }) {
108
108
  type: "button",
109
109
  onClick: l,
110
110
  disabled: u,
111
- className: "\n px-6 py-2.5\n bg-transparent\n text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-bg-subtle)]\n active:scale-[0.98]\n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[140px]\n ",
111
+ className: "\n px-6 py-2.5\n bg-transparent\n text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[140px]\n ",
112
112
  children: d("common.cancel", "Cancel")
113
113
  })]
114
114
  })
@@ -1 +1 @@
1
- {"version":3,"file":"InviteMemberForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/InviteMemberForm.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Mail, Send } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface InviteMemberFormProps {\n organizationId: string;\n onSubmit: (data: { organizationId: string; email: string; message?: string }) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\n/**\n * Invite Member Form\n * Email-based form for inviting users to an organization.\n * Role defaults to MEMBER on the backend — admin can change it after the user joins.\n */\nexport function InviteMemberForm({\n organizationId,\n onSubmit,\n onCancel,\n loading,\n}: InviteMemberFormProps) {\n const tr = useTr();\n const [formData, setFormData] = useState({\n email: '',\n message: '',\n });\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n try {\n await onSubmit({\n organizationId,\n email: formData.email,\n message: formData.message || undefined,\n });\n setFormData({ email: '', message: '' });\n } catch {\n // Error is surfaced by the parent page via the mutation hook's error state\n }\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Email */}\n <div>\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.emailLabel', 'Email Address')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n <Mail size={16} className=\"text-[var(--color-text-muted)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={(e) => setFormData({ ...formData, email: e.target.value })}\n aria-label={tr('members.invite.emailLabel', 'Email Address')}\n className=\"\n w-full pl-10 pr-4 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('members.invite.emailPlaceholder', 'user@example.com')}\n disabled={loading}\n required\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-muted)] mt-1\">\n {tr(\n 'members.invite.emailHelp',\n 'The user will be invited as a Member. You can change their role after they join.'\n )}\n </p>\n </div>\n\n {/* Message (optional) */}\n <div>\n <label\n htmlFor=\"message\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.messageLabel', 'Personal Message')}{' '}\n <span className=\"text-[var(--color-text-muted)]\">\n ({tr('common.optional', 'optional')})\n </span>\n </label>\n <textarea\n id=\"message\"\n value={formData.message}\n onChange={(e) => setFormData({ ...formData, message: e.target.value })}\n aria-label={tr('members.invite.messageLabel', 'Personal Message')}\n className=\"\n w-full px-4 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n resize-none\n \"\n placeholder={tr(\n 'members.invite.messagePlaceholder',\n 'Add a personal message to the invite...'\n )}\n rows={3}\n maxLength={500}\n disabled={loading}\n />\n </div>\n\n {/* Actions */}\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"\n inline-flex items-center gap-2 px-6 py-2.5\n bg-[var(--color-action-primary-bg)]\n text-[var(--color-action-primary-text)]\n rounded-lg\n hover:opacity-90 hover:scale-[1.02]\n active:scale-[0.98]\n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[180px]\n \"\n >\n {loading ? <Loader2 size={16} className=\"animate-spin\" /> : <Send size={16} />}\n {tr('members.invite.send', 'Send Invite')}\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={loading}\n className=\"\n px-6 py-2.5\n bg-transparent\n text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-bg-subtle)]\n active:scale-[0.98]\n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[140px]\n \"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </form>\n );\n}\n"],"mappings":";;;;;AAgBA,SAAgB,EAAiB,EAC/B,mBACA,aACA,aACA,cACwB;CACxB,IAAM,IAAK,GAAO,EACZ,CAAC,GAAU,KAAe,EAAS;EACvC,OAAO;EACP,SAAS;EACV,CAAC;AAgBF,QACE,kBAAC,QAAD;EAAM,UAfa,OAAO,MAAuB;AACjD,KAAE,gBAAgB;AAClB,OAAI;AAMF,IALA,MAAM,EAAS;KACb;KACA,OAAO,EAAS;KAChB,SAAS,EAAS,WAAW,KAAA;KAC9B,CAAC,EACF,EAAY;KAAE,OAAO;KAAI,SAAS;KAAI,CAAC;WACjC;;EAMsB,WAAU;YAAxC;GAEE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KACE,SAAQ;KACR,WAAU;eAFZ;MAIG,EAAG,6BAA6B,gBAAgB;MAAE;MACnD,kBAAC,QAAD;OAAM,WAAU;iBAAwC;OAAQ,CAAA;MAC1D;;IACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAM,MAAM;OAAI,WAAU;OAAmC,CAAA;MACzD,CAAA,EACN,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO,EAAS;MAChB,WAAW,MAAM,EAAY;OAAE,GAAG;OAAU,OAAO,EAAE,OAAO;OAAO,CAAC;MACpE,cAAY,EAAG,6BAA6B,gBAAgB;MAC5D,WAAU;MAUV,aAAa,EAAG,mCAAmC,mBAAmB;MACtE,UAAU;MACV,UAAA;MACA,CAAA,CACE;;IACN,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,4BACA,mFACD;KACC,CAAA;IACA,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAFZ;KAIG,EAAG,+BAA+B,mBAAmB;KAAE;KACxD,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAAiD;OAC7C,EAAG,mBAAmB,WAAW;OAAC;OAC/B;;KACD;OACR,kBAAC,YAAD;IACE,IAAG;IACH,OAAO,EAAS;IAChB,WAAW,MAAM,EAAY;KAAE,GAAG;KAAU,SAAS,EAAE,OAAO;KAAO,CAAC;IACtE,cAAY,EAAG,+BAA+B,mBAAmB;IACjE,WAAU;IAWV,aAAa,EACX,qCACA,0CACD;IACD,MAAM;IACN,WAAW;IACX,UAAU;IACV,CAAA,CACE,EAAA,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,WAAU;eAHZ,CAgBG,IAAU,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAC7E,EAAG,uBAAuB,cAAc,CAClC;QAET,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU;KACV,WAAU;eAcT,EAAG,iBAAiB,SAAS;KACvB,CAAA,CACL;;GACD"}
1
+ {"version":3,"file":"InviteMemberForm.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/InviteMemberForm.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Loader2, Mail, Send } from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface InviteMemberFormProps {\n organizationId: string;\n onSubmit: (data: { organizationId: string; email: string; message?: string }) => Promise<void>;\n onCancel: () => void;\n loading?: boolean;\n}\n\n/**\n * Invite Member Form\n * Email-based form for inviting users to an organization.\n * Role defaults to MEMBER on the backend — admin can change it after the user joins.\n */\nexport function InviteMemberForm({\n organizationId,\n onSubmit,\n onCancel,\n loading,\n}: InviteMemberFormProps) {\n const tr = useTr();\n const [formData, setFormData] = useState({\n email: '',\n message: '',\n });\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n try {\n await onSubmit({\n organizationId,\n email: formData.email,\n message: formData.message || undefined,\n });\n setFormData({ email: '', message: '' });\n } catch {\n // Error is surfaced by the parent page via the mutation hook's error state\n }\n };\n\n return (\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n {/* Email */}\n <div>\n <label\n htmlFor=\"email\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.emailLabel', 'Email Address')}{' '}\n <span className=\"text-[var(--color-status-error-text)]\">*</span>\n </label>\n <div className=\"relative\">\n <div className=\"absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none\">\n <Mail size={16} className=\"text-[var(--color-text-muted)]\" />\n </div>\n <input\n id=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={(e) => setFormData({ ...formData, email: e.target.value })}\n aria-label={tr('members.invite.emailLabel', 'Email Address')}\n className=\"\n w-full pl-10 pr-4 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n \"\n placeholder={tr('members.invite.emailPlaceholder', 'user@example.com')}\n disabled={loading}\n required\n />\n </div>\n <p className=\"text-xs text-[var(--color-text-muted)] mt-1\">\n {tr(\n 'members.invite.emailHelp',\n 'The user will be invited as a Member. You can change their role after they join.'\n )}\n </p>\n </div>\n\n {/* Message (optional) */}\n <div>\n <label\n htmlFor=\"message\"\n className=\"block text-sm font-medium text-[var(--color-text-primary)] mb-1\"\n >\n {tr('members.invite.messageLabel', 'Personal Message')}{' '}\n <span className=\"text-[var(--color-text-muted)]\">\n ({tr('common.optional', 'optional')})\n </span>\n </label>\n <textarea\n id=\"message\"\n value={formData.message}\n onChange={(e) => setFormData({ ...formData, message: e.target.value })}\n aria-label={tr('members.invite.messageLabel', 'Personal Message')}\n className=\"\n w-full px-4 py-2\n bg-[var(--color-bg-surface)]\n border border-[var(--color-border-default)]\n rounded-lg\n text-[var(--color-text-primary)]\n placeholder:text-[var(--color-text-muted)]\n focus:outline-none focus:ring-2 focus:ring-[var(--color-border-focus)]\n disabled:bg-[var(--color-bg-muted)] disabled:cursor-not-allowed\n resize-none\n \"\n placeholder={tr(\n 'members.invite.messagePlaceholder',\n 'Add a personal message to the invite...'\n )}\n rows={3}\n maxLength={500}\n disabled={loading}\n />\n </div>\n\n {/* Actions */}\n <div className=\"flex flex-col gap-3 border-t border-[var(--color-border-default)] pt-4 sm:flex-row\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"\n inline-flex items-center gap-2 px-6 py-2.5\n bg-[var(--color-action-primary-bg)]\n text-[var(--color-action-primary-text)]\n rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[180px]\n \"\n >\n {loading ? <Loader2 size={16} className=\"animate-spin\" /> : <Send size={16} />}\n {tr('members.invite.send', 'Send Invite')}\n </button>\n\n <button\n type=\"button\"\n onClick={onCancel}\n disabled={loading}\n className=\"\n px-6 py-2.5\n bg-transparent\n text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-bg-subtle)]\n \n transition-all duration-200\n font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n sm:min-w-[140px]\n \"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n </div>\n </form>\n );\n}\n"],"mappings":";;;;;AAgBA,SAAgB,EAAiB,EAC/B,mBACA,aACA,aACA,cACwB;CACxB,IAAM,IAAK,GAAO,EACZ,CAAC,GAAU,KAAe,EAAS;EACvC,OAAO;EACP,SAAS;EACV,CAAC;AAgBF,QACE,kBAAC,QAAD;EAAM,UAfa,OAAO,MAAuB;AACjD,KAAE,gBAAgB;AAClB,OAAI;AAMF,IALA,MAAM,EAAS;KACb;KACA,OAAO,EAAS;KAChB,SAAS,EAAS,WAAW,KAAA;KAC9B,CAAC,EACF,EAAY;KAAE,OAAO;KAAI,SAAS;KAAI,CAAC;WACjC;;EAMsB,WAAU;YAAxC;GAEE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KACE,SAAQ;KACR,WAAU;eAFZ;MAIG,EAAG,6BAA6B,gBAAgB;MAAE;MACnD,kBAAC,QAAD;OAAM,WAAU;iBAAwC;OAAQ,CAAA;MAC1D;;IACR,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAM,MAAM;OAAI,WAAU;OAAmC,CAAA;MACzD,CAAA,EACN,kBAAC,SAAD;MACE,IAAG;MACH,MAAK;MACL,OAAO,EAAS;MAChB,WAAW,MAAM,EAAY;OAAE,GAAG;OAAU,OAAO,EAAE,OAAO;OAAO,CAAC;MACpE,cAAY,EAAG,6BAA6B,gBAAgB;MAC5D,WAAU;MAUV,aAAa,EAAG,mCAAmC,mBAAmB;MACtE,UAAU;MACV,UAAA;MACA,CAAA,CACE;;IACN,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,4BACA,mFACD;KACC,CAAA;IACA,EAAA,CAAA;GAGN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,SAAD;IACE,SAAQ;IACR,WAAU;cAFZ;KAIG,EAAG,+BAA+B,mBAAmB;KAAE;KACxD,kBAAC,QAAD;MAAM,WAAU;gBAAhB;OAAiD;OAC7C,EAAG,mBAAmB,WAAW;OAAC;OAC/B;;KACD;OACR,kBAAC,YAAD;IACE,IAAG;IACH,OAAO,EAAS;IAChB,WAAW,MAAM,EAAY;KAAE,GAAG;KAAU,SAAS,EAAE,OAAO;KAAO,CAAC;IACtE,cAAY,EAAG,+BAA+B,mBAAmB;IACjE,WAAU;IAWV,aAAa,EACX,qCACA,0CACD;IACD,MAAM;IACN,WAAW;IACX,UAAU;IACV,CAAA,CACE,EAAA,CAAA;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU;KACV,WAAU;eAHZ,CAgBG,IAAU,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAC7E,EAAG,uBAAuB,cAAc,CAClC;QAET,kBAAC,UAAD;KACE,MAAK;KACL,SAAS;KACT,UAAU;KACV,WAAU;eAcT,EAAG,iBAAiB,SAAS;KACvB,CAAA,CACL;;GACD"}
@@ -47,7 +47,7 @@ function l({ invite: l, onAccept: u, onReject: d, accepting: f, rejecting: p })
47
47
  type: "button",
48
48
  onClick: () => u(l.id),
49
49
  disabled: f || p,
50
- className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n active:scale-[0.98]\n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n ",
50
+ className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n ",
51
51
  children: [f ? /* @__PURE__ */ s(a, {
52
52
  size: 14,
53
53
  className: "animate-spin"
@@ -56,7 +56,7 @@ function l({ invite: l, onAccept: u, onReject: d, accepting: f, rejecting: p })
56
56
  type: "button",
57
57
  onClick: () => d(l.id),
58
58
  disabled: f || p,
59
- className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n active:scale-[0.98]\n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n ",
59
+ className: "\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n ",
60
60
  children: [p ? /* @__PURE__ */ s(a, {
61
61
  size: 14,
62
62
  className: "animate-spin"
@@ -1 +1 @@
1
- {"version":3,"file":"MyInviteCard.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/MyInviteCard.tsx"],"sourcesContent":["import { Check, X, Building2, Clock, Loader2 } from 'lucide-react';\nimport { MemberRoleBadge } from './MemberRoleBadge';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface MyInviteCardProps {\n invite: {\n id: string;\n email: string;\n role: string;\n expiresAt: string;\n message?: string | null;\n createdAt: string;\n organization?: {\n id: string;\n name: string;\n slug: string;\n } | null;\n };\n onAccept: (inviteId: string) => Promise<void>;\n onReject: (inviteId: string) => Promise<void>;\n accepting?: boolean;\n rejecting?: boolean;\n}\n\nexport function MyInviteCard({\n invite,\n onAccept,\n onReject,\n accepting,\n rejecting,\n}: MyInviteCardProps) {\n const tr = useTr();\n const daysUntilExpiry = Math.max(\n 0,\n Math.ceil((new Date(invite.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))\n );\n\n return (\n <div className=\"rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-5 transition-colors hover:border-[var(--color-border-focus)]\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n {/* Left: Org info */}\n <div className=\"flex items-start gap-3 min-w-0\">\n <div className=\"p-2 rounded-lg bg-[var(--color-bg-subtle)]\">\n <Building2 size={20} className=\"text-[var(--color-text-secondary)]\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"font-semibold text-[var(--color-text-primary)] truncate\">\n {invite.organization?.name ?? tr('common.organization', 'Organization')}\n </h3>\n <div className=\"flex items-center gap-2 mt-1\">\n <MemberRoleBadge role={invite.role} />\n <span className=\"text-xs text-[var(--color-text-muted)] flex items-center gap-1\">\n <Clock size={12} />\n {daysUntilExpiry > 0\n ? `${tr('members.myInvites.expiresIn', 'Expires in')} ${daysUntilExpiry}d`\n : tr('members.myInvites.expiringSoon', 'Expiring soon')}\n </span>\n </div>\n {invite.message && (\n <p className=\"mt-2 text-sm text-[var(--color-text-secondary)] italic\">\n &ldquo;{invite.message}&rdquo;\n </p>\n )}\n </div>\n </div>\n\n {/* Right: Actions */}\n <div className=\"flex flex-wrap items-center gap-2 shrink-0\">\n <button\n type=\"button\"\n onClick={() => onAccept(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n active:scale-[0.98]\n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n \"\n >\n {accepting ? <Loader2 size={14} className=\"animate-spin\" /> : <Check size={14} />}\n {tr('members.myInvites.accept', 'Accept')}\n </button>\n <button\n type=\"button\"\n onClick={() => onReject(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n active:scale-[0.98]\n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n \"\n >\n {rejecting ? <Loader2 size={14} className=\"animate-spin\" /> : <X size={14} />}\n {tr('members.myInvites.decline', 'Decline')}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,EAAa,EAC3B,WACA,aACA,aACA,cACA,gBACoB;CACpB,IAAM,IAAK,GAAO,EACZ,IAAkB,KAAK,IAC3B,GACA,KAAK,MAAM,IAAI,KAAK,EAAO,UAAU,CAAC,SAAS,GAAG,KAAK,KAAK,KAAK,MAAO,KAAK,KAAK,IAAI,CACvF;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAW,MAAM;MAAI,WAAU;MAAuC,CAAA;KAClE,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAO,cAAc,QAAQ,EAAG,uBAAuB,eAAe;OACpE,CAAA;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAiB,MAAM,EAAO,MAAQ,CAAA,EACtC,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAClB,IAAkB,IACf,GAAG,EAAG,+BAA+B,aAAa,CAAC,GAAG,EAAgB,KACtE,EAAG,kCAAkC,gBAAgB,CACpD;UACH;;MACL,EAAO,WACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QAAsE;QAC5D,EAAO;QAAQ;QACrB;;MAEF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,EAAO,GAAG;KAClC,UAAU,KAAa;KACvB,WAAU;eAJZ,CAcG,IAAY,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAChF,EAAG,4BAA4B,SAAS,CAClC;QACT,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,EAAO,GAAG;KAClC,UAAU,KAAa;KACvB,WAAU;eAJZ,CAgBG,IAAY,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA,EAC5E,EAAG,6BAA6B,UAAU,CACpC;OACL;MACF;;EACF,CAAA"}
1
+ {"version":3,"file":"MyInviteCard.js","names":[],"sources":["../../../../../src/organization-management/modules/members/components/MyInviteCard.tsx"],"sourcesContent":["import { Check, X, Building2, Clock, Loader2 } from 'lucide-react';\nimport { MemberRoleBadge } from './MemberRoleBadge';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface MyInviteCardProps {\n invite: {\n id: string;\n email: string;\n role: string;\n expiresAt: string;\n message?: string | null;\n createdAt: string;\n organization?: {\n id: string;\n name: string;\n slug: string;\n } | null;\n };\n onAccept: (inviteId: string) => Promise<void>;\n onReject: (inviteId: string) => Promise<void>;\n accepting?: boolean;\n rejecting?: boolean;\n}\n\nexport function MyInviteCard({\n invite,\n onAccept,\n onReject,\n accepting,\n rejecting,\n}: MyInviteCardProps) {\n const tr = useTr();\n const daysUntilExpiry = Math.max(\n 0,\n Math.ceil((new Date(invite.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))\n );\n\n return (\n <div className=\"rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] p-5 transition-colors hover:border-[var(--color-border-focus)]\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n {/* Left: Org info */}\n <div className=\"flex items-start gap-3 min-w-0\">\n <div className=\"p-2 rounded-lg bg-[var(--color-bg-subtle)]\">\n <Building2 size={20} className=\"text-[var(--color-text-secondary)]\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"font-semibold text-[var(--color-text-primary)] truncate\">\n {invite.organization?.name ?? tr('common.organization', 'Organization')}\n </h3>\n <div className=\"flex items-center gap-2 mt-1\">\n <MemberRoleBadge role={invite.role} />\n <span className=\"text-xs text-[var(--color-text-muted)] flex items-center gap-1\">\n <Clock size={12} />\n {daysUntilExpiry > 0\n ? `${tr('members.myInvites.expiresIn', 'Expires in')} ${daysUntilExpiry}d`\n : tr('members.myInvites.expiringSoon', 'Expiring soon')}\n </span>\n </div>\n {invite.message && (\n <p className=\"mt-2 text-sm text-[var(--color-text-secondary)] italic\">\n &ldquo;{invite.message}&rdquo;\n </p>\n )}\n </div>\n </div>\n\n {/* Right: Actions */}\n <div className=\"flex flex-wrap items-center gap-2 shrink-0\">\n <button\n type=\"button\"\n onClick={() => onAccept(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-[var(--color-status-success-bg)] text-[var(--color-text-inverse)] rounded-lg\n hover:opacity-90\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n \"\n >\n {accepting ? <Loader2 size={14} className=\"animate-spin\" /> : <Check size={14} />}\n {tr('members.myInvites.accept', 'Accept')}\n </button>\n <button\n type=\"button\"\n onClick={() => onReject(invite.id)}\n disabled={accepting || rejecting}\n className=\"\n inline-flex items-center gap-1.5 px-4 py-2\n bg-transparent text-[var(--color-text-primary)]\n border border-[var(--color-border-default)]\n rounded-lg\n hover:bg-[var(--color-status-error-bgSubtle)] hover:border-[var(--color-status-error-border)] hover:text-[var(--color-status-error-text)]\n \n transition-all duration-200\n text-sm font-medium\n disabled:opacity-50 disabled:cursor-not-allowed\n \"\n >\n {rejecting ? <Loader2 size={14} className=\"animate-spin\" /> : <X size={14} />}\n {tr('members.myInvites.decline', 'Decline')}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,EAAa,EAC3B,WACA,aACA,aACA,cACA,gBACoB;CACpB,IAAM,IAAK,GAAO,EACZ,IAAkB,KAAK,IAC3B,GACA,KAAK,MAAM,IAAI,KAAK,EAAO,UAAU,CAAC,SAAS,GAAG,KAAK,KAAK,KAAK,MAAO,KAAK,KAAK,IAAI,CACvF;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,OAAD;GAAK,WAAU;aAAf,CAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAW,MAAM;MAAI,WAAU;MAAuC,CAAA;KAClE,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAO,cAAc,QAAQ,EAAG,uBAAuB,eAAe;OACpE,CAAA;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAiB,MAAM,EAAO,MAAQ,CAAA,EACtC,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAClB,IAAkB,IACf,GAAG,EAAG,+BAA+B,aAAa,CAAC,GAAG,EAAgB,KACtE,EAAG,kCAAkC,gBAAgB,CACpD;UACH;;MACL,EAAO,WACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QAAsE;QAC5D,EAAO;QAAQ;QACrB;;MAEF;OACF;OAGN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,EAAO,GAAG;KAClC,UAAU,KAAa;KACvB,WAAU;eAJZ,CAcG,IAAY,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAChF,EAAG,4BAA4B,SAAS,CAClC;QACT,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAS,EAAO,GAAG;KAClC,UAAU,KAAa;KACvB,WAAU;eAJZ,CAgBG,IAAY,kBAAC,GAAD;MAAS,MAAM;MAAI,WAAU;MAAiB,CAAA,GAAG,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA,EAC5E,EAAG,6BAA6B,UAAU,CACpC;OACL;MACF;;EACF,CAAA"}