@burdenoff/microfe-workspaces 2026.825.1 → 2026.829.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.
- package/dist/pages/MembersListPage.js +3 -3
- package/dist/pages/MembersListPage.js.map +1 -1
- package/dist/pages/ProjectsListPage.js +3 -3
- package/dist/pages/ProjectsListPage.js.map +1 -1
- package/dist/pages/WorkspaceInvitePage.js +2 -2
- package/dist/pages/WorkspaceInvitePage.js.map +1 -1
- package/package.json +3 -3
|
@@ -108,10 +108,10 @@ function _() {
|
|
|
108
108
|
if (G.length === 0) return;
|
|
109
109
|
te("medium");
|
|
110
110
|
let e = `${i(_, "membersList.bulkRemoveConfirmPrefix", "Remove")} ${G.length} ${G.length === 1 ? i(_, "membersList.bulkRemoveConfirmSingular", "selected member") : i(_, "membersList.bulkRemoveConfirmPlural", "selected members")} ${i(_, "membersList.bulkRemoveConfirmSuffix", "from this workspace?")}`, t = await ee({
|
|
111
|
-
title: "Remove members",
|
|
111
|
+
title: i(_, "membersList.removeMembersTitle", "Remove members"),
|
|
112
112
|
message: e,
|
|
113
|
-
okButtonTitle: "Remove",
|
|
114
|
-
cancelButtonTitle: "Cancel"
|
|
113
|
+
okButtonTitle: i(_, "membersList.removeButton", "Remove"),
|
|
114
|
+
cancelButtonTitle: i(_, "common.cancel", "Cancel")
|
|
115
115
|
});
|
|
116
116
|
(t === null ? window.confirm(e) : t) && (G.forEach((e) => {
|
|
117
117
|
Ee(e.userId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MembersListPage.js","names":[],"sources":["../../src/pages/MembersListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useNavigate, useParams, useSearchParams } from 'react-router';\nimport { Users, UserPlus, Search, AlertCircle, RefreshCw } from 'lucide-react';\nimport {\n Button,\n Input,\n Skeleton,\n Badge,\n PageContainer,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useActorProfiles, type ActorInput, type ActorProfileMap } from '@burdenoff/fe-libs/shared';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\nimport { PermissionButton } from '@burdenoff/fe-libs/shared/components';\nimport { WORKSPACE_PERMISSIONS } from '../constants/permissions';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { useWorkspaceMembers, useWorkspaceInvitations } from '../hooks/useWorkspaceMembers';\nimport { useRemoveUserFromWorkspace, useResendInvitation } from '../hooks/useMemberMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { MemberListItem } from '../components/MemberListItem';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\n\nexport function MembersListPage() {\n const { t } = useI18n();\n\n const {\n memberFilters,\n setMemberFilters,\n selectedMemberIds,\n toggleMemberSelection,\n clearMemberSelection,\n } = useWorkspacesStore();\n const navigate = useNavigate();\n const { workspaceId: contextWorkspaceId } = useWorkspacesContext();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { workspaceId: urlWorkspaceId } = useParams<{ workspaceId: string }>();\n\n // Prefer URL workspaceId over context (user navigated to specific workspace).\n // WorkspaceContextPathSync keeps this URL segment in step with the active context\n // on a context switch, so the override can't point at a stale workspace.\n // (BOFF-2805 follow-up)\n const workspaceId = urlWorkspaceId || contextWorkspaceId;\n\n useWorkspacesPageView('workspaces.members', { workspaceId, requireParams: true });\n\n const [searchValue, setSearchValue] = useState(memberFilters.search || '');\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setMemberFilters({ search: debouncedSearch });\n }, [debouncedSearch, setMemberFilters]);\n\n // Fetch members and invitations\n const {\n data: membersData,\n isLoading: isLoadingMembers,\n error: membersError,\n } = useWorkspaceMembers(workspaceId || '');\n\n const { data: invitationsData, isLoading: isLoadingInvitations } = useWorkspaceInvitations(\n workspaceId || '',\n 'PENDING'\n );\n\n const { mutate: resendInvitation, isPending: isResending } = useResendInvitation();\n const { mutate: removeUserFromWorkspace, isPending: isRemovingMembers } =\n useRemoveUserFromWorkspace();\n const [resendingId, setResendingId] = useState<string | null>(null);\n const [selectedInvitationIds, setSelectedInvitationIds] = useState<Set<string>>(new Set());\n\n const handleResendInvitation = (invitationId: string) => {\n setResendingId(invitationId);\n resendInvitation(invitationId, {\n onSuccess: () => {\n setResendingId(null);\n },\n onError: (error) => {\n console.error('Failed to resend invitation:', error);\n setResendingId(null);\n },\n });\n };\n\n const members = useMemo(() => membersData?.items || [], [membersData?.items]);\n const pendingInvitations = useMemo(() => invitationsData?.items || [], [invitationsData?.items]);\n const isLoading = isLoadingMembers || isLoadingInvitations;\n const hasMembers = members.length > 0;\n const hasPendingInvitations = pendingInvitations.length > 0;\n\n // Resolve every member's userId to displayName + email via the shared\n // ActorProfile cache so search can match human-friendly fields instead of\n // raw UUIDs.\n const { apiGatewayUrl, authToken, organizationId } = useWorkspacesContext();\n const memberActors = useMemo<ActorInput[]>(\n () => members.map((m) => ({ actorId: m.userId, actorType: 'user' })),\n [members]\n );\n const profileMap: ActorProfileMap = useActorProfiles(memberActors, {\n apiGatewayUrl,\n authToken,\n orgId: organizationId,\n workspaceId,\n });\n\n // Filter members by resolved identity (displayName, email) — fall back to\n // raw userId only if the profile hasn't resolved yet so search still works\n // during the brief loading window.\n const filteredMembers = members.filter((member) => {\n if (!debouncedSearch) return true;\n const needle = debouncedSearch.toLowerCase();\n\n // Primary: the shared ActorProfile cache — canonical, works for every\n // member regardless of how they joined.\n const profile = profileMap[member.userId];\n if (profile) {\n if (profile.displayName.toLowerCase().includes(needle)) return true;\n if (profile.email?.toLowerCase().includes(needle)) return true;\n }\n\n // Fallback: the denormalized identity surfaced by the backend through\n // `WorkspaceMember.context.metadata` (populated from\n // `WorkspaceMember.email` for members who joined via invitation — PR #66).\n // Available synchronously, so search still matches during the brief window\n // before the ActorProfile lookup resolves.\n //\n // The metadata field is typed `Record<string, unknown>`, so we MUST guard\n // every read with `typeof === 'string'`. A non-string value (e.g. a number,\n // null, or a nested object accidentally surfaced by a future backend\n // mapper) would otherwise crash the page on `.toLowerCase()`. Reviewed:\n // PR #24 codex (P2).\n const meta = member.context?.metadata;\n const metaEmail = typeof meta?.email === 'string' ? meta.email : '';\n const metaDisplayRaw =\n typeof meta?.displayName === 'string'\n ? meta.displayName\n : typeof meta?.name === 'string'\n ? meta.name\n : '';\n if (metaEmail.toLowerCase().includes(needle)) return true;\n if (metaDisplayRaw.toLowerCase().includes(needle)) return true;\n\n // Last resort: raw userId — keeps the previous behaviour for members with\n // neither a resolved profile nor a denormalized identity yet.\n return member.userId.toLowerCase().includes(needle);\n });\n\n const selectedMembers = useMemo(\n () => filteredMembers.filter((member) => selectedMemberIds.has(member.id)),\n [filteredMembers, selectedMemberIds]\n );\n\n const selectedInvitations = useMemo(\n () => pendingInvitations.filter((invitation) => selectedInvitationIds.has(invitation.id)),\n [pendingInvitations, selectedInvitationIds]\n );\n\n const allVisibleMembersSelected =\n filteredMembers.length > 0 &&\n filteredMembers.every((member) => selectedMemberIds.has(member.id));\n const partiallySelectedMembers =\n filteredMembers.some((member) => selectedMemberIds.has(member.id)) &&\n !allVisibleMembersSelected;\n\n const allVisibleInvitationsSelected =\n pendingInvitations.length > 0 &&\n pendingInvitations.every((invitation) => selectedInvitationIds.has(invitation.id));\n const partiallySelectedInvitations =\n pendingInvitations.some((invitation) => selectedInvitationIds.has(invitation.id)) &&\n !allVisibleInvitationsSelected;\n\n // Clear selection on unmount\n useEffect(() => {\n return () => clearMemberSelection();\n }, [clearMemberSelection]);\n\n // Redirect to the canonical workspace invite page when arriving through\n // older members deep links that used ?invite=true.\n const [searchParams, setSearchParams] = useSearchParams();\n useEffect(() => {\n if (searchParams.get('invite') === 'true' && workspaceId) {\n const next = new URLSearchParams(searchParams);\n next.delete('invite');\n setSearchParams(next, { replace: true });\n navigate(withCurrentSearch(`${basePath}/${workspaceId}/invite`), { replace: true });\n }\n }, [searchParams, setSearchParams, workspaceId, navigate, withCurrentSearch, basePath]);\n\n const {\n canListMembers,\n canInviteMember,\n canRemoveMember,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <Skeleton className=\"h-64\" />\n </div>\n );\n }\n\n if (!canListMembers && !canInviteMember) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'membersList.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'membersList.noPermissionToViewMembers',\n \"You don't have permission to view workspace members.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const handleInviteMember = () => {\n if (workspaceId) {\n navigate(withCurrentSearch(`${basePath}/${workspaceId}/invite`));\n }\n };\n\n const toggleAllMembers = (checked: boolean) => {\n filteredMembers.forEach((member) => {\n const isSelected = selectedMemberIds.has(member.id);\n if (checked !== isSelected) {\n toggleMemberSelection(member.id);\n }\n });\n };\n\n const toggleInvitationSelection = (invitationId: string, checked: boolean) => {\n setSelectedInvitationIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(invitationId);\n } else {\n next.delete(invitationId);\n }\n return next;\n });\n };\n\n const toggleAllInvitations = (checked: boolean) => {\n setSelectedInvitationIds((prev) => {\n const next = new Set(prev);\n pendingInvitations.forEach((invitation) => {\n if (checked) {\n next.add(invitation.id);\n } else {\n next.delete(invitation.id);\n }\n });\n return next;\n });\n };\n\n const handleBulkRemoveMembers = async () => {\n if (selectedMembers.length === 0) return;\n void nativeImpact('medium');\n const message = `${tWithFallback(t, 'membersList.bulkRemoveConfirmPrefix', 'Remove')} ${selectedMembers.length} ${\n selectedMembers.length === 1\n ? tWithFallback(t, 'membersList.bulkRemoveConfirmSingular', 'selected member')\n : tWithFallback(t, 'membersList.bulkRemoveConfirmPlural', 'selected members')\n } ${tWithFallback(t, 'membersList.bulkRemoveConfirmSuffix', 'from this workspace?')}`;\n const native = await nativeConfirm({\n title: 'Remove members',\n message,\n okButtonTitle: 'Remove',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n selectedMembers.forEach((member) => {\n removeUserFromWorkspace(member.userId);\n });\n clearMemberSelection();\n };\n\n const handleBulkResendInvitations = () => {\n if (selectedInvitations.length === 0) return;\n selectedInvitations.forEach((invitation) => {\n resendInvitation(invitation.id, {\n onSuccess: () => {\n setSelectedInvitationIds((prev) => {\n const next = new Set(prev);\n next.delete(invitation.id);\n return next;\n });\n },\n });\n });\n };\n\n return (\n <PageContainer padded={false}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'membersList.pageTitle', 'Members')}\n </h1>\n <p className=\"text-text-secondary text-sm sm:text-base\">\n {tWithFallback(\n t,\n 'membersList.pageSubtitle',\n 'Manage workspace members and invitations'\n )}\n </p>\n </div>\n\n <div className=\"flex gap-2\">\n <PermissionButton\n permission={WORKSPACE_PERMISSIONS.MEMBER_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tWithFallback(\n t,\n 'membersList.noPermissionToInvite',\n \"You don't have permission to invite members\"\n )}\n >\n <Button onClick={handleInviteMember} disabled={!workspaceId}>\n <UserPlus className=\"size-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'membersList.inviteMember', 'Invite Member')}\n </span>\n </Button>\n </PermissionButton>\n </div>\n </div>\n\n <PagePurpose>\n {tWithFallback(\n t,\n 'membersList.purpose',\n 'Members are the people who belong to this workspace. From here you invite teammates, see who has accepted, resend or track invitations that are still pending, and remove people who no longer need access. Search by name or email to find someone quickly.'\n )}\n </PagePurpose>\n\n {/* Search & Stats */}\n <div className=\"flex flex-col sm:flex-row gap-3 sm:gap-4\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-secondary\" />\n <Input\n placeholder={tWithFallback(t, 'membersList.searchPlaceholder', 'Search members...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n\n {hasPendingInvitations && (\n <div className=\"flex items-center gap-2\">\n <Badge variant=\"secondary\" className=\"text-sm\">\n {pendingInvitations.length}{' '}\n {pendingInvitations.length !== 1\n ? tWithFallback(t, 'membersList.pendingInvitations', 'Pending Invitations')\n : tWithFallback(t, 'membersList.pendingInvitation', 'Pending Invitation')}\n </Badge>\n </div>\n )}\n </div>\n\n {filteredMembers.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleMembersSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelectedMembers && !allVisibleMembersSelected;\n }\n }}\n onChange={(event) => toggleAllMembers(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'membersList.selectAllMembersAriaLabel',\n 'Select all workspace members'\n )}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>\n {tWithFallback(\n t,\n 'membersList.selectAllVisibleMembers',\n 'Select all visible members'\n )}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedMemberIds.size} {tWithFallback(t, 'membersList.selected', 'selected')}\n </span>\n {selectedMemberIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearMemberSelection}>\n {tWithFallback(t, 'membersList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {canRemoveMember && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkRemoveMembers}\n disabled={selectedMembers.length === 0 || isRemovingMembers}\n className=\"text-status-error-text\"\n >\n {tWithFallback(t, 'membersList.removeSelected', 'Remove Selected')}\n {selectedMembers.length > 0 ? ` (${selectedMembers.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"space-y-3\">\n {[...Array(5)].map((_, i) => (\n <Skeleton key={i} className=\"h-20 w-full\" />\n ))}\n </div>\n )}\n\n {/* Error State */}\n {!isLoading && membersError && (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-status-error-bg/10 p-6 mb-4\">\n <AlertCircle size={48} className=\"text-status-error-text\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'membersList.failedToLoad', 'Failed to Load Members')}\n </h3>\n <p className=\"text-text-secondary max-w-sm\">\n {membersError instanceof Error\n ? membersError.message\n : tWithFallback(\n t,\n 'membersList.errorLoadingMembers',\n 'An error occurred while loading members'\n )}\n </p>\n </div>\n )}\n\n {/* Empty State - No Workspace */}\n {!isLoading && !membersError && !workspaceId && (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tWithFallback(t, 'membersList.noWorkspaceSelected', 'No Workspace Selected')}\n description={tWithFallback(\n t,\n 'membersList.selectWorkspacePrompt',\n 'Select a workspace to view and manage its members'\n )}\n />\n )}\n\n {/* Empty State - No Members */}\n {!isLoading && !membersError && workspaceId && !hasMembers && (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tWithFallback(t, 'membersList.noMembersYet', 'No Members Yet')}\n description={tWithFallback(\n t,\n 'membersList.noMembersPrompt',\n 'Start building your team by inviting members to this workspace'\n )}\n action={\n <PermissionButton\n permission={WORKSPACE_PERMISSIONS.MEMBER_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tWithFallback(\n t,\n 'membersList.noPermissionToInvite',\n \"You don't have permission to invite members\"\n )}\n >\n <Button onClick={handleInviteMember}>\n <UserPlus className=\"mr-2 size-4\" />\n {tWithFallback(t, 'membersList.inviteFirstMember', 'Invite Your First Member')}\n </Button>\n </PermissionButton>\n }\n />\n )}\n\n {/* Empty State - No Search Results */}\n {!isLoading &&\n !membersError &&\n workspaceId &&\n hasMembers &&\n filteredMembers.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={tWithFallback(t, 'membersList.noMembersFound', 'No Members Found')}\n description={tWithFallback(\n t,\n 'membersList.noMembersFoundPrompt',\n 'No members match your search criteria. Try adjusting your search.'\n )}\n />\n )}\n\n {/* Members List */}\n {!isLoading && filteredMembers.length > 0 && (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'membersList.activeMembers', 'Active Members')} (\n {filteredMembers.length})\n </h2>\n </div>\n <div className=\"space-y-2\">\n {filteredMembers.map((member) => (\n <MemberListItem\n key={member.id}\n member={member}\n selected={selectedMemberIds.has(member.id)}\n onToggleSelect={(checked) => {\n const isSelected = selectedMemberIds.has(member.id);\n if (checked !== isSelected) {\n toggleMemberSelection(member.id);\n }\n }}\n canRemove={canRemoveMember}\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Pending Invitations Section - Now at bottom */}\n {!isLoading && hasPendingInvitations && (\n <div className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'membersList.pendingInvitationsHeader', 'Pending Invitations')}\n </h2>\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleInvitationsSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate =\n partiallySelectedInvitations && !allVisibleInvitationsSelected;\n }\n }}\n onChange={(event) => toggleAllInvitations(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'membersList.selectAllInvitationsAriaLabel',\n 'Select all pending invitations'\n )}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>\n {tWithFallback(t, 'membersList.selectAllInvitations', 'Select all invitations')}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedInvitationIds.size}{' '}\n {tWithFallback(t, 'membersList.selected', 'selected')}\n </span>\n {selectedInvitationIds.size > 0 && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setSelectedInvitationIds(new Set())}\n >\n {tWithFallback(t, 'membersList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {canInviteMember && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkResendInvitations}\n disabled={selectedInvitations.length === 0 || isResending}\n >\n {tWithFallback(t, 'membersList.resendSelected', 'Resend Selected')}\n {selectedInvitations.length > 0 ? ` (${selectedInvitations.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n <div className=\"space-y-2\">\n {pendingInvitations.map((invitation) => (\n <div\n key={invitation.id}\n className=\"flex items-center justify-between p-4 border rounded-lg bg-bg-surface\"\n >\n <div className=\"flex items-center gap-3\">\n <input\n type=\"checkbox\"\n checked={selectedInvitationIds.has(invitation.id)}\n onChange={(event) =>\n toggleInvitationSelection(invitation.id, event.target.checked)\n }\n aria-label={`${tWithFallback(t, 'membersList.selectInvitationAriaLabel', 'Select invitation')} ${invitation.email}`}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <div className=\"size-10 rounded-full bg-status-warning/10 flex items-center justify-center transition-colors\">\n <Users className=\"size-5 text-status-warning\" />\n </div>\n <div>\n <p className=\"font-medium text-sm\">{invitation.email}</p>\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(t, 'membersList.invited', 'Invited')}{' '}\n {new Date(invitation.createdAt).toLocaleDateString()}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <PermissionButton\n permission={WORKSPACE_PERMISSIONS.MEMBER_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tWithFallback(\n t,\n 'membersList.noPermissionToResend',\n \"You don't have permission to resend invitations\"\n )}\n >\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleResendInvitation(invitation.id)}\n disabled={isResending && resendingId === invitation.id}\n >\n <RefreshCw\n className={`size-4 mr-1 ${isResending && resendingId === invitation.id ? 'animate-spin' : ''}`}\n />\n {isResending && resendingId === invitation.id\n ? tWithFallback(t, 'membersList.sending', 'Sending...')\n : tWithFallback(t, 'membersList.resend', 'Resend')}\n </Button>\n </PermissionButton>\n <Badge\n variant=\"outline\"\n className=\"bg-status-warning/10 text-status-warning border-status-warning/30 transition-colors\"\n >\n {tWithFallback(t, 'membersList.pending', 'Pending')}\n </Badge>\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n </PageContainer>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MembersListPage.js","names":[],"sources":["../../src/pages/MembersListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useNavigate, useParams, useSearchParams } from 'react-router';\nimport { Users, UserPlus, Search, AlertCircle, RefreshCw } from 'lucide-react';\nimport {\n Button,\n Input,\n Skeleton,\n Badge,\n PageContainer,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useActorProfiles, type ActorInput, type ActorProfileMap } from '@burdenoff/fe-libs/shared';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\nimport { PermissionButton } from '@burdenoff/fe-libs/shared/components';\nimport { WORKSPACE_PERMISSIONS } from '../constants/permissions';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { useWorkspaceMembers, useWorkspaceInvitations } from '../hooks/useWorkspaceMembers';\nimport { useRemoveUserFromWorkspace, useResendInvitation } from '../hooks/useMemberMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { MemberListItem } from '../components/MemberListItem';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\n\nexport function MembersListPage() {\n const { t } = useI18n();\n\n const {\n memberFilters,\n setMemberFilters,\n selectedMemberIds,\n toggleMemberSelection,\n clearMemberSelection,\n } = useWorkspacesStore();\n const navigate = useNavigate();\n const { workspaceId: contextWorkspaceId } = useWorkspacesContext();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { workspaceId: urlWorkspaceId } = useParams<{ workspaceId: string }>();\n\n // Prefer URL workspaceId over context (user navigated to specific workspace).\n // WorkspaceContextPathSync keeps this URL segment in step with the active context\n // on a context switch, so the override can't point at a stale workspace.\n // (BOFF-2805 follow-up)\n const workspaceId = urlWorkspaceId || contextWorkspaceId;\n\n useWorkspacesPageView('workspaces.members', { workspaceId, requireParams: true });\n\n const [searchValue, setSearchValue] = useState(memberFilters.search || '');\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setMemberFilters({ search: debouncedSearch });\n }, [debouncedSearch, setMemberFilters]);\n\n // Fetch members and invitations\n const {\n data: membersData,\n isLoading: isLoadingMembers,\n error: membersError,\n } = useWorkspaceMembers(workspaceId || '');\n\n const { data: invitationsData, isLoading: isLoadingInvitations } = useWorkspaceInvitations(\n workspaceId || '',\n 'PENDING'\n );\n\n const { mutate: resendInvitation, isPending: isResending } = useResendInvitation();\n const { mutate: removeUserFromWorkspace, isPending: isRemovingMembers } =\n useRemoveUserFromWorkspace();\n const [resendingId, setResendingId] = useState<string | null>(null);\n const [selectedInvitationIds, setSelectedInvitationIds] = useState<Set<string>>(new Set());\n\n const handleResendInvitation = (invitationId: string) => {\n setResendingId(invitationId);\n resendInvitation(invitationId, {\n onSuccess: () => {\n setResendingId(null);\n },\n onError: (error) => {\n console.error('Failed to resend invitation:', error);\n setResendingId(null);\n },\n });\n };\n\n const members = useMemo(() => membersData?.items || [], [membersData?.items]);\n const pendingInvitations = useMemo(() => invitationsData?.items || [], [invitationsData?.items]);\n const isLoading = isLoadingMembers || isLoadingInvitations;\n const hasMembers = members.length > 0;\n const hasPendingInvitations = pendingInvitations.length > 0;\n\n // Resolve every member's userId to displayName + email via the shared\n // ActorProfile cache so search can match human-friendly fields instead of\n // raw UUIDs.\n const { apiGatewayUrl, authToken, organizationId } = useWorkspacesContext();\n const memberActors = useMemo<ActorInput[]>(\n () => members.map((m) => ({ actorId: m.userId, actorType: 'user' })),\n [members]\n );\n const profileMap: ActorProfileMap = useActorProfiles(memberActors, {\n apiGatewayUrl,\n authToken,\n orgId: organizationId,\n workspaceId,\n });\n\n // Filter members by resolved identity (displayName, email) — fall back to\n // raw userId only if the profile hasn't resolved yet so search still works\n // during the brief loading window.\n const filteredMembers = members.filter((member) => {\n if (!debouncedSearch) return true;\n const needle = debouncedSearch.toLowerCase();\n\n // Primary: the shared ActorProfile cache — canonical, works for every\n // member regardless of how they joined.\n const profile = profileMap[member.userId];\n if (profile) {\n if (profile.displayName.toLowerCase().includes(needle)) return true;\n if (profile.email?.toLowerCase().includes(needle)) return true;\n }\n\n // Fallback: the denormalized identity surfaced by the backend through\n // `WorkspaceMember.context.metadata` (populated from\n // `WorkspaceMember.email` for members who joined via invitation — PR #66).\n // Available synchronously, so search still matches during the brief window\n // before the ActorProfile lookup resolves.\n //\n // The metadata field is typed `Record<string, unknown>`, so we MUST guard\n // every read with `typeof === 'string'`. A non-string value (e.g. a number,\n // null, or a nested object accidentally surfaced by a future backend\n // mapper) would otherwise crash the page on `.toLowerCase()`. Reviewed:\n // PR #24 codex (P2).\n const meta = member.context?.metadata;\n const metaEmail = typeof meta?.email === 'string' ? meta.email : '';\n const metaDisplayRaw =\n typeof meta?.displayName === 'string'\n ? meta.displayName\n : typeof meta?.name === 'string'\n ? meta.name\n : '';\n if (metaEmail.toLowerCase().includes(needle)) return true;\n if (metaDisplayRaw.toLowerCase().includes(needle)) return true;\n\n // Last resort: raw userId — keeps the previous behaviour for members with\n // neither a resolved profile nor a denormalized identity yet.\n return member.userId.toLowerCase().includes(needle);\n });\n\n const selectedMembers = useMemo(\n () => filteredMembers.filter((member) => selectedMemberIds.has(member.id)),\n [filteredMembers, selectedMemberIds]\n );\n\n const selectedInvitations = useMemo(\n () => pendingInvitations.filter((invitation) => selectedInvitationIds.has(invitation.id)),\n [pendingInvitations, selectedInvitationIds]\n );\n\n const allVisibleMembersSelected =\n filteredMembers.length > 0 &&\n filteredMembers.every((member) => selectedMemberIds.has(member.id));\n const partiallySelectedMembers =\n filteredMembers.some((member) => selectedMemberIds.has(member.id)) &&\n !allVisibleMembersSelected;\n\n const allVisibleInvitationsSelected =\n pendingInvitations.length > 0 &&\n pendingInvitations.every((invitation) => selectedInvitationIds.has(invitation.id));\n const partiallySelectedInvitations =\n pendingInvitations.some((invitation) => selectedInvitationIds.has(invitation.id)) &&\n !allVisibleInvitationsSelected;\n\n // Clear selection on unmount\n useEffect(() => {\n return () => clearMemberSelection();\n }, [clearMemberSelection]);\n\n // Redirect to the canonical workspace invite page when arriving through\n // older members deep links that used ?invite=true.\n const [searchParams, setSearchParams] = useSearchParams();\n useEffect(() => {\n if (searchParams.get('invite') === 'true' && workspaceId) {\n const next = new URLSearchParams(searchParams);\n next.delete('invite');\n setSearchParams(next, { replace: true });\n navigate(withCurrentSearch(`${basePath}/${workspaceId}/invite`), { replace: true });\n }\n }, [searchParams, setSearchParams, workspaceId, navigate, withCurrentSearch, basePath]);\n\n const {\n canListMembers,\n canInviteMember,\n canRemoveMember,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <Skeleton className=\"h-64\" />\n </div>\n );\n }\n\n if (!canListMembers && !canInviteMember) {\n return (\n <div className=\"flex items-center justify-center min-h-[400px]\">\n <div className=\"text-center space-y-2\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'membersList.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'membersList.noPermissionToViewMembers',\n \"You don't have permission to view workspace members.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n const handleInviteMember = () => {\n if (workspaceId) {\n navigate(withCurrentSearch(`${basePath}/${workspaceId}/invite`));\n }\n };\n\n const toggleAllMembers = (checked: boolean) => {\n filteredMembers.forEach((member) => {\n const isSelected = selectedMemberIds.has(member.id);\n if (checked !== isSelected) {\n toggleMemberSelection(member.id);\n }\n });\n };\n\n const toggleInvitationSelection = (invitationId: string, checked: boolean) => {\n setSelectedInvitationIds((prev) => {\n const next = new Set(prev);\n if (checked) {\n next.add(invitationId);\n } else {\n next.delete(invitationId);\n }\n return next;\n });\n };\n\n const toggleAllInvitations = (checked: boolean) => {\n setSelectedInvitationIds((prev) => {\n const next = new Set(prev);\n pendingInvitations.forEach((invitation) => {\n if (checked) {\n next.add(invitation.id);\n } else {\n next.delete(invitation.id);\n }\n });\n return next;\n });\n };\n\n const handleBulkRemoveMembers = async () => {\n if (selectedMembers.length === 0) return;\n void nativeImpact('medium');\n const message = `${tWithFallback(t, 'membersList.bulkRemoveConfirmPrefix', 'Remove')} ${selectedMembers.length} ${\n selectedMembers.length === 1\n ? tWithFallback(t, 'membersList.bulkRemoveConfirmSingular', 'selected member')\n : tWithFallback(t, 'membersList.bulkRemoveConfirmPlural', 'selected members')\n } ${tWithFallback(t, 'membersList.bulkRemoveConfirmSuffix', 'from this workspace?')}`;\n const native = await nativeConfirm({\n title: tWithFallback(t, 'membersList.removeMembersTitle', 'Remove members'),\n message,\n okButtonTitle: tWithFallback(t, 'membersList.removeButton', 'Remove'),\n cancelButtonTitle: tWithFallback(t, 'common.cancel', 'Cancel'),\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n selectedMembers.forEach((member) => {\n removeUserFromWorkspace(member.userId);\n });\n clearMemberSelection();\n };\n\n const handleBulkResendInvitations = () => {\n if (selectedInvitations.length === 0) return;\n selectedInvitations.forEach((invitation) => {\n resendInvitation(invitation.id, {\n onSuccess: () => {\n setSelectedInvitationIds((prev) => {\n const next = new Set(prev);\n next.delete(invitation.id);\n return next;\n });\n },\n });\n });\n };\n\n return (\n <PageContainer padded={false}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'membersList.pageTitle', 'Members')}\n </h1>\n <p className=\"text-text-secondary text-sm sm:text-base\">\n {tWithFallback(\n t,\n 'membersList.pageSubtitle',\n 'Manage workspace members and invitations'\n )}\n </p>\n </div>\n\n <div className=\"flex gap-2\">\n <PermissionButton\n permission={WORKSPACE_PERMISSIONS.MEMBER_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tWithFallback(\n t,\n 'membersList.noPermissionToInvite',\n \"You don't have permission to invite members\"\n )}\n >\n <Button onClick={handleInviteMember} disabled={!workspaceId}>\n <UserPlus className=\"size-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'membersList.inviteMember', 'Invite Member')}\n </span>\n </Button>\n </PermissionButton>\n </div>\n </div>\n\n <PagePurpose>\n {tWithFallback(\n t,\n 'membersList.purpose',\n 'Members are the people who belong to this workspace. From here you invite teammates, see who has accepted, resend or track invitations that are still pending, and remove people who no longer need access. Search by name or email to find someone quickly.'\n )}\n </PagePurpose>\n\n {/* Search & Stats */}\n <div className=\"flex flex-col sm:flex-row gap-3 sm:gap-4\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 size-4 text-text-secondary\" />\n <Input\n placeholder={tWithFallback(t, 'membersList.searchPlaceholder', 'Search members...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-9\"\n />\n </div>\n\n {hasPendingInvitations && (\n <div className=\"flex items-center gap-2\">\n <Badge variant=\"secondary\" className=\"text-sm\">\n {pendingInvitations.length}{' '}\n {pendingInvitations.length !== 1\n ? tWithFallback(t, 'membersList.pendingInvitations', 'Pending Invitations')\n : tWithFallback(t, 'membersList.pendingInvitation', 'Pending Invitation')}\n </Badge>\n </div>\n )}\n </div>\n\n {filteredMembers.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleMembersSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelectedMembers && !allVisibleMembersSelected;\n }\n }}\n onChange={(event) => toggleAllMembers(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'membersList.selectAllMembersAriaLabel',\n 'Select all workspace members'\n )}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>\n {tWithFallback(\n t,\n 'membersList.selectAllVisibleMembers',\n 'Select all visible members'\n )}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedMemberIds.size} {tWithFallback(t, 'membersList.selected', 'selected')}\n </span>\n {selectedMemberIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearMemberSelection}>\n {tWithFallback(t, 'membersList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {canRemoveMember && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkRemoveMembers}\n disabled={selectedMembers.length === 0 || isRemovingMembers}\n className=\"text-status-error-text\"\n >\n {tWithFallback(t, 'membersList.removeSelected', 'Remove Selected')}\n {selectedMembers.length > 0 ? ` (${selectedMembers.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"space-y-3\">\n {[...Array(5)].map((_, i) => (\n <Skeleton key={i} className=\"h-20 w-full\" />\n ))}\n </div>\n )}\n\n {/* Error State */}\n {!isLoading && membersError && (\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-status-error-bg/10 p-6 mb-4\">\n <AlertCircle size={48} className=\"text-status-error-text\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'membersList.failedToLoad', 'Failed to Load Members')}\n </h3>\n <p className=\"text-text-secondary max-w-sm\">\n {membersError instanceof Error\n ? membersError.message\n : tWithFallback(\n t,\n 'membersList.errorLoadingMembers',\n 'An error occurred while loading members'\n )}\n </p>\n </div>\n )}\n\n {/* Empty State - No Workspace */}\n {!isLoading && !membersError && !workspaceId && (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tWithFallback(t, 'membersList.noWorkspaceSelected', 'No Workspace Selected')}\n description={tWithFallback(\n t,\n 'membersList.selectWorkspacePrompt',\n 'Select a workspace to view and manage its members'\n )}\n />\n )}\n\n {/* Empty State - No Members */}\n {!isLoading && !membersError && workspaceId && !hasMembers && (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title={tWithFallback(t, 'membersList.noMembersYet', 'No Members Yet')}\n description={tWithFallback(\n t,\n 'membersList.noMembersPrompt',\n 'Start building your team by inviting members to this workspace'\n )}\n action={\n <PermissionButton\n permission={WORKSPACE_PERMISSIONS.MEMBER_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tWithFallback(\n t,\n 'membersList.noPermissionToInvite',\n \"You don't have permission to invite members\"\n )}\n >\n <Button onClick={handleInviteMember}>\n <UserPlus className=\"mr-2 size-4\" />\n {tWithFallback(t, 'membersList.inviteFirstMember', 'Invite Your First Member')}\n </Button>\n </PermissionButton>\n }\n />\n )}\n\n {/* Empty State - No Search Results */}\n {!isLoading &&\n !membersError &&\n workspaceId &&\n hasMembers &&\n filteredMembers.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title={tWithFallback(t, 'membersList.noMembersFound', 'No Members Found')}\n description={tWithFallback(\n t,\n 'membersList.noMembersFoundPrompt',\n 'No members match your search criteria. Try adjusting your search.'\n )}\n />\n )}\n\n {/* Members List */}\n {!isLoading && filteredMembers.length > 0 && (\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'membersList.activeMembers', 'Active Members')} (\n {filteredMembers.length})\n </h2>\n </div>\n <div className=\"space-y-2\">\n {filteredMembers.map((member) => (\n <MemberListItem\n key={member.id}\n member={member}\n selected={selectedMemberIds.has(member.id)}\n onToggleSelect={(checked) => {\n const isSelected = selectedMemberIds.has(member.id);\n if (checked !== isSelected) {\n toggleMemberSelection(member.id);\n }\n }}\n canRemove={canRemoveMember}\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Pending Invitations Section - Now at bottom */}\n {!isLoading && hasPendingInvitations && (\n <div className=\"space-y-3\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'membersList.pendingInvitationsHeader', 'Pending Invitations')}\n </h2>\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleInvitationsSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate =\n partiallySelectedInvitations && !allVisibleInvitationsSelected;\n }\n }}\n onChange={(event) => toggleAllInvitations(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'membersList.selectAllInvitationsAriaLabel',\n 'Select all pending invitations'\n )}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>\n {tWithFallback(t, 'membersList.selectAllInvitations', 'Select all invitations')}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedInvitationIds.size}{' '}\n {tWithFallback(t, 'membersList.selected', 'selected')}\n </span>\n {selectedInvitationIds.size > 0 && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => setSelectedInvitationIds(new Set())}\n >\n {tWithFallback(t, 'membersList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {canInviteMember && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkResendInvitations}\n disabled={selectedInvitations.length === 0 || isResending}\n >\n {tWithFallback(t, 'membersList.resendSelected', 'Resend Selected')}\n {selectedInvitations.length > 0 ? ` (${selectedInvitations.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n <div className=\"space-y-2\">\n {pendingInvitations.map((invitation) => (\n <div\n key={invitation.id}\n className=\"flex items-center justify-between p-4 border rounded-lg bg-bg-surface\"\n >\n <div className=\"flex items-center gap-3\">\n <input\n type=\"checkbox\"\n checked={selectedInvitationIds.has(invitation.id)}\n onChange={(event) =>\n toggleInvitationSelection(invitation.id, event.target.checked)\n }\n aria-label={`${tWithFallback(t, 'membersList.selectInvitationAriaLabel', 'Select invitation')} ${invitation.email}`}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <div className=\"size-10 rounded-full bg-status-warning/10 flex items-center justify-center transition-colors\">\n <Users className=\"size-5 text-status-warning\" />\n </div>\n <div>\n <p className=\"font-medium text-sm\">{invitation.email}</p>\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(t, 'membersList.invited', 'Invited')}{' '}\n {new Date(invitation.createdAt).toLocaleDateString()}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <PermissionButton\n permission={WORKSPACE_PERMISSIONS.MEMBER_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip={tWithFallback(\n t,\n 'membersList.noPermissionToResend',\n \"You don't have permission to resend invitations\"\n )}\n >\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => handleResendInvitation(invitation.id)}\n disabled={isResending && resendingId === invitation.id}\n >\n <RefreshCw\n className={`size-4 mr-1 ${isResending && resendingId === invitation.id ? 'animate-spin' : ''}`}\n />\n {isResending && resendingId === invitation.id\n ? tWithFallback(t, 'membersList.sending', 'Sending...')\n : tWithFallback(t, 'membersList.resend', 'Resend')}\n </Button>\n </PermissionButton>\n <Badge\n variant=\"outline\"\n className=\"bg-status-warning/10 text-status-warning border-status-warning/30 transition-colors\"\n >\n {tWithFallback(t, 'membersList.pending', 'Pending')}\n </Badge>\n </div>\n </div>\n ))}\n </div>\n </div>\n )}\n </div>\n </PageContainer>\n );\n}\n"],"mappings":"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"}
|
|
@@ -354,10 +354,10 @@ function D() {
|
|
|
354
354
|
if (q.length === 0) return;
|
|
355
355
|
i("medium");
|
|
356
356
|
let e = t(D, "projectsList.bulkDeleteConfirmPrefix", "Delete ") + q.length + " " + t(D, "projectsList.bulkDeleteConfirmProject", "selected project") + (q.length === 1 ? "" : t(D, "projectsList.bulkDeleteConfirmPluralSuffix", "s")) + t(D, "projectsList.bulkDeleteConfirmSuffix", "? This cannot be undone."), n = await r({
|
|
357
|
-
title: "Delete projects",
|
|
357
|
+
title: t(D, "projectsList.deleteProjectsTitle", "Delete projects"),
|
|
358
358
|
message: e,
|
|
359
|
-
okButtonTitle: "Delete",
|
|
360
|
-
cancelButtonTitle: "Cancel"
|
|
359
|
+
okButtonTitle: t(D, "common.delete", "Delete"),
|
|
360
|
+
cancelButtonTitle: t(D, "common.cancel", "Cancel")
|
|
361
361
|
});
|
|
362
362
|
(n === null ? window.confirm(e) : n) && (await Promise.allSettled(q.map((e) => ye(e.id))), F());
|
|
363
363
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsListPage.js","names":[],"sources":["../../src/pages/ProjectsListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Plus, Grid3x3, List, Search } from 'lucide-react';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Button,\n Input,\n Card,\n CardContent,\n GlassCard,\n PageContainer,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useMyProjects } from '../hooks/useProjects';\nimport {\n useArchiveProject,\n useDeleteProject,\n useUnarchiveProject,\n} from '../hooks/useProjectMutations';\nimport { ProjectCard } from '../components/ProjectCard';\nimport { CreateProjectDialog } from '../components/CreateProjectDialog';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { tWithFallback } from '../utils/i18n';\nimport { getProjectDisplayTags } from '../utils/projectTags';\nimport type { ProjectTag, Workspace } from '../types';\n\nexport function ProjectsListPage() {\n const { t } = useI18n();\n\n // The URL path `:workspaceId` (when present) intentionally OVERRIDES the active\n // context — it scopes this page to a specific workspace for deep-links/drill-in.\n // The cross-workspace route (`/workspaces/projects`, no path param) leaves it\n // undefined so the list stays cross-workspace. The stale-path-after-context-switch\n // problem is handled by WorkspaceContextPathSync (it rewrites this segment when the\n // active context changes), so the override never points at a stale workspace.\n // (BOFF-2805 follow-up)\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.projects', { workspaceId, requireParams: true });\n const {\n viewMode,\n setViewMode,\n projectFilters,\n setProjectFilters,\n clearProjectFilters,\n selectedProjectIds,\n toggleProjectSelection,\n clearProjectSelection,\n openCreateProjectDialog,\n isCreateProjectDialogOpen,\n } = useWorkspacesStore();\n\n const [searchValue, setSearchValue] = useState(projectFilters.search || '');\n const [activeTagFilters, setActiveTagFilters] = useState<ProjectTag[]>([]);\n const [matchAllTagFilters, setMatchAllTagFilters] = useState(true);\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setProjectFilters({ search: debouncedSearch });\n }, [debouncedSearch, setProjectFilters]);\n\n useEffect(() => {\n if (activeTagFilters.length === 0) {\n setProjectFilters({\n tagIds: undefined,\n tagHierarchyPaths: undefined,\n matchAllTags: undefined,\n includeDescendants: undefined,\n });\n return;\n }\n\n setProjectFilters({\n tagIds: activeTagFilters.map((tag) => tag.tagId),\n tagHierarchyPaths: activeTagFilters\n .map((tag) => tag.hierarchyPath)\n .filter((tagPath): tagPath is string => typeof tagPath === 'string' && tagPath.length > 0),\n matchAllTags: matchAllTagFilters,\n includeDescendants: true,\n });\n }, [activeTagFilters, matchAllTagFilters, setProjectFilters]);\n\n useEffect(() => {\n return () => clearProjectSelection();\n }, [clearProjectSelection]);\n\n // Membership-scoped query: backend filters by actorId, so no client-side\n // permission gate is needed. Page is shared by `/workspaces/projects` and\n // `/workspaces/:workspaceId/projects`; the latter narrows the list below.\n const PROJECTS_PAGE_SIZE = 500;\n const {\n data: myData,\n isLoading,\n error,\n } = useMyProjects({\n limit: PROJECTS_PAGE_SIZE,\n offset: 0,\n });\n\n const data = useMemo(() => {\n const all = myData?.items ?? [];\n const q = (projectFilters.search ?? '').trim().toLowerCase();\n const tagIds = projectFilters.tagIds ?? [];\n const matchAllTags = projectFilters.matchAllTags ?? true;\n\n const matchesWorkspace = (project: (typeof all)[number]) =>\n !workspaceId || project.workspaceId === workspaceId;\n const matchesSearch = (project: (typeof all)[number]) =>\n !q || project.name.toLowerCase().includes(q) || project.key.toLowerCase().includes(q);\n const matchesStatus = (project: (typeof all)[number]) =>\n !projectFilters.status || project.status === projectFilters.status;\n const matchesArchived = (project: (typeof all)[number]) =>\n typeof projectFilters.archived === 'undefined' ||\n project.archived === projectFilters.archived;\n const matchesTags = (project: (typeof all)[number]) => {\n if (tagIds.length === 0) return true;\n const projectTagIds = new Set(getProjectDisplayTags(project).map((t) => t.tagId));\n return matchAllTags\n ? tagIds.every((id) => projectTagIds.has(id))\n : tagIds.some((id) => projectTagIds.has(id));\n };\n\n const filtered = all.filter(\n (project) =>\n matchesWorkspace(project) &&\n matchesSearch(project) &&\n matchesStatus(project) &&\n matchesArchived(project) &&\n matchesTags(project)\n );\n\n // Surface the real backend totals so the \"Showing X of Y\" footer can flag\n // truncation. For narrowed views (workspace/tag filter), the backend total\n // doesn't reflect the post-filter count, so we report the filtered size as\n // total. But we MUST preserve `hasMore` from upstream — if the upstream\n // query was capped (hasMore=true), matches outside the first page are\n // silently hidden, and the footer needs to keep flagging that.\n const apiTotal = myData?.total ?? filtered.length;\n const apiHasMore = myData?.hasMore ?? false;\n const isNarrowed = !!workspaceId || tagIds.length > 0;\n return {\n items: filtered,\n total: isNarrowed ? filtered.length : apiTotal,\n hasMore: apiHasMore,\n };\n }, [myData?.items, myData?.total, myData?.hasMore, projectFilters, workspaceId]);\n\n const { mutateAsync: archiveProject } = useArchiveProject(workspaceId);\n const { mutateAsync: unarchiveProject } = useUnarchiveProject(workspaceId);\n const { mutateAsync: deleteProject } = useDeleteProject(workspaceId);\n\n const selectedProjects = useMemo(\n () => data?.items.filter((project) => selectedProjectIds.has(project.id)) ?? [],\n [data?.items, selectedProjectIds]\n );\n\n const archivableProjects = useMemo(\n () => selectedProjects.filter((project) => !project.archived),\n [selectedProjects]\n );\n\n const unarchivableProjects = useMemo(\n () => selectedProjects.filter((project) => project.archived),\n [selectedProjects]\n );\n\n const allVisibleSelected =\n (data?.items.length ?? 0) > 0 &&\n (data?.items ?? []).every((project) => selectedProjectIds.has(project.id));\n const partiallySelected =\n (data?.items ?? []).some((project) => selectedProjectIds.has(project.id)) &&\n !allVisibleSelected;\n\n const {\n canCreateProject,\n canUpdateProject,\n canDeleteProject,\n canArchiveProject,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n );\n }\n\n // No client-side list gate: this page calls `myProjects` which is\n // membership-scoped — the resolver filters by actorId on the backend, so the\n // data set returned IS the access list. If the user is in zero projects, the\n // page renders the empty state below. Per-action checks (canCreateProject,\n // canDeleteProject, canArchiveProject) remain in place to hide buttons the\n // user isn't authorized for on their workspace.\n\n const handleCreateProject = () => {\n openCreateProjectDialog({} as Workspace);\n };\n\n const handleStatusFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.status;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ status: value as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED' });\n }\n };\n\n const handleArchivedFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.archived;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ archived: value === 'true' });\n }\n };\n\n const toggleTagFilter = (tag: ProjectTag) => {\n setActiveTagFilters((current) => {\n const exists = current.some((entry) => entry.tagId === tag.tagId);\n if (exists) {\n return current.filter((entry) => entry.tagId !== tag.tagId);\n }\n\n return [...current, tag];\n });\n };\n\n const clearAllProjectFilters = () => {\n setSearchValue('');\n setActiveTagFilters([]);\n setMatchAllTagFilters(true);\n clearProjectFilters();\n };\n\n const toggleAllProjects = (checked: boolean) => {\n (data?.items ?? []).forEach((project) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n });\n };\n\n const handleBulkArchive = async () => {\n if (archivableProjects.length === 0) return;\n await Promise.allSettled(archivableProjects.map((project) => archiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkUnarchive = async () => {\n if (unarchivableProjects.length === 0) return;\n await Promise.allSettled(unarchivableProjects.map((project) => unarchiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkDelete = async () => {\n if (selectedProjects.length === 0) return;\n void nativeImpact('medium');\n const message =\n tWithFallback(t, 'projectsList.bulkDeleteConfirmPrefix', 'Delete ') +\n selectedProjects.length +\n ' ' +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmProject', 'selected project') +\n (selectedProjects.length === 1\n ? ''\n : tWithFallback(t, 'projectsList.bulkDeleteConfirmPluralSuffix', 's')) +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmSuffix', '? This cannot be undone.');\n const native = await nativeConfirm({\n title: 'Delete projects',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n await Promise.allSettled(selectedProjects.map((project) => deleteProject(project.id)));\n clearProjectSelection();\n };\n\n return (\n <PageContainer padded={false}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'projectsList.pageTitle', 'Projects')}\n </h1>\n <p className=\"text-text-secondary\">\n {tWithFallback(t, 'projectsList.pageSubtitle', 'Manage and organize your projects')}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* View mode toggle */}\n <div className=\"flex items-center gap-1 border rounded-lg p-1\">\n <Button\n variant={viewMode === 'grid' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('grid')}\n className=\"size-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.gridViewAriaLabel',\n 'Show projects in grid view'\n )}\n >\n <Grid3x3 size={16} />\n </Button>\n <Button\n variant={viewMode === 'list' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('list')}\n className=\"size-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.listViewAriaLabel',\n 'Show projects in list view'\n )}\n >\n <List size={16} />\n </Button>\n </div>\n {canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus className=\"size-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n <PagePurpose>\n {tWithFallback(\n t,\n 'projectsList.purpose',\n 'Projects are the individual streams of work inside your workspaces — a launch, a client engagement, a feature. Each carries its own members, tags and activity. Use search, status and tag filters to find the one you need, or create a new project to start tracking work.'\n )}\n </PagePurpose>\n\n {/* Filters & Search */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"relative flex-1\">\n <Search\n className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-text-secondary\"\n size={18}\n />\n <Input\n placeholder={tWithFallback(t, 'projectsList.searchPlaceholder', 'Search projects...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-[3.25rem]\"\n />\n </div>\n <Select value={projectFilters.status || 'all'} onValueChange={handleStatusFilter}>\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByStatus', 'Filter by status')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.statusAll', 'All Statuses')}\n </SelectItem>\n <SelectItem value=\"ACTIVE\">\n {tWithFallback(t, 'projectsList.statusActive', 'Active')}\n </SelectItem>\n <SelectItem value=\"INACTIVE\">\n {tWithFallback(t, 'projectsList.statusInactive', 'Inactive')}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select\n value={\n projectFilters.archived === undefined\n ? 'all'\n : projectFilters.archived\n ? 'true'\n : 'false'\n }\n onValueChange={handleArchivedFilter}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByArchive', 'Filter by archive')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.archiveAll', 'All Projects')}\n </SelectItem>\n <SelectItem value=\"false\">\n {tWithFallback(t, 'projectsList.archiveActiveProjects', 'Active Projects')}\n </SelectItem>\n <SelectItem value=\"true\">\n {tWithFallback(t, 'projectsList.archiveArchived', 'Archived')}\n </SelectItem>\n </SelectContent>\n </Select>\n {(projectFilters.search ||\n projectFilters.status ||\n projectFilters.archived !== undefined ||\n activeTagFilters.length > 0) && (\n <Button variant=\"outline\" onClick={clearAllProjectFilters}>\n {tWithFallback(t, 'projectsList.clearFilters', 'Clear Filters')}\n </Button>\n )}\n </div>\n\n {activeTagFilters.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface px-4 py-3\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-medium\">\n {tWithFallback(t, 'projectsList.activeTagFilters', 'Active tag filters')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(\n t,\n 'projectsList.activeTagFiltersDescription',\n 'Click a project tag to add or remove it from the current filter set.'\n )}\n </p>\n </div>\n <Select\n value={matchAllTagFilters ? 'all' : 'any'}\n onValueChange={(value) => setMatchAllTagFilters(value === 'all')}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.tagMatchMode', 'Tag match mode')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.tagMatchAll', 'Match all tags')}\n </SelectItem>\n <SelectItem value=\"any\">\n {tWithFallback(t, 'projectsList.tagMatchAny', 'Match any tag')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {activeTagFilters.map((tag) => (\n <TagBadge\n key={tag.tagId}\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => toggleTagFilter(tag)}\n size=\"sm\"\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Error State */}\n {error && (\n <GlassCard treatment=\"plain\" className=\"border-status-error-border\">\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-status-error-text font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.errorLoading', 'Error loading projects')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'projectsList.unknownError', 'An unknown error occurred')}\n </p>\n </div>\n </CardContent>\n </GlassCard>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <Card key={i} className=\"border-border-seam\">\n <CardContent className=\"pt-6\">\n <div className=\"space-y-3\">\n <Skeleton className=\"size-12 rounded-lg\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && data?.items.length === 0 && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <CardContent className=\"pt-6\">\n <IllustratedEmptyState\n illustration={projectFilters.search ? 'empty-search' : 'empty-data'}\n title={tWithFallback(t, 'projectsList.noProjectsFound', 'No projects found')}\n description={\n projectFilters.search\n ? tWithFallback(\n t,\n 'projectsList.adjustSearchOrFilters',\n 'Try adjusting your search or filters'\n )\n : tWithFallback(\n t,\n 'projectsList.createFirstProject',\n 'Create your first project to get started organizing your work'\n )\n }\n action={\n !projectFilters.search && canCreateProject ? (\n <Button onClick={handleCreateProject}>\n <Plus size={16} className=\"mr-2\" />\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </Button>\n ) : undefined\n }\n />\n </CardContent>\n </GlassCard>\n )}\n\n {/* Projects Grid */}\n {!isLoading && !error && data && data.items.length > 0 && (\n <>\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllProjects(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'projectsList.selectAllAriaLabel',\n 'Select all projects'\n )}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>\n {tWithFallback(t, 'projectsList.selectAllVisible', 'Select all visible')}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedProjectIds.size} {tWithFallback(t, 'projectsList.selected', 'selected')}\n </span>\n {selectedProjectIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearProjectSelection}>\n {tWithFallback(t, 'projectsList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {/* Bulk mutations are bound to a single workspaceId via the\n archive/unarchive/delete hooks. On the cross-workspace\n /workspaces/projects route (workspaceId is undefined),\n hide them — selections can span workspaces and the bound\n hook would fail authorization for projects outside the\n caller's context. The per-card actions in ProjectCard\n still work because they resolve per-project. */}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkArchive}\n disabled={archivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.archiveSelected', 'Archive Selected')}\n {archivableProjects.length > 0 ? ` (${archivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkUnarchive}\n disabled={unarchivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.unarchiveSelected', 'Unarchive Selected')}\n {unarchivableProjects.length > 0 ? ` (${unarchivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canDeleteProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkDelete}\n disabled={selectedProjects.length === 0}\n className=\"text-status-error-text\"\n >\n {tWithFallback(t, 'projectsList.deleteSelected', 'Delete Selected')}\n {selectedProjects.length > 0 ? ` (${selectedProjects.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n\n <div\n className={\n viewMode === 'list'\n ? 'space-y-3'\n : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'\n }\n >\n {data.items.map((project) => (\n <ProjectCard\n key={project.id}\n project={project}\n selected={selectedProjectIds.has(project.id)}\n canArchive={canArchiveProject}\n canDelete={canDeleteProject}\n canRename={canUpdateProject}\n onTagClick={toggleTagFilter}\n onToggleSelect={(checked) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n }}\n />\n ))}\n </div>\n\n {/* Pagination Info */}\n {data.total > data.items.length && (\n <div className=\"text-center text-sm text-text-secondary\">\n {tWithFallback(t, 'projectsList.showingPrefix', 'Showing')} {data.items.length}{' '}\n {tWithFallback(t, 'projectsList.showingOf', 'of')} {data.total}{' '}\n {tWithFallback(t, 'projectsList.showingProjects', 'projects')}\n </div>\n )}\n </>\n )}\n\n {/* Create Project Dialog */}\n {isCreateProjectDialogOpen && <CreateProjectDialog workspaceId={workspaceId} />}\n </div>\n </PageContainer>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectsListPage.js","names":[],"sources":["../../src/pages/ProjectsListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router';\nimport { Plus, Grid3x3, List, Search } from 'lucide-react';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Button,\n Input,\n Card,\n CardContent,\n GlassCard,\n PageContainer,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useMyProjects } from '../hooks/useProjects';\nimport {\n useArchiveProject,\n useDeleteProject,\n useUnarchiveProject,\n} from '../hooks/useProjectMutations';\nimport { ProjectCard } from '../components/ProjectCard';\nimport { CreateProjectDialog } from '../components/CreateProjectDialog';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { tWithFallback } from '../utils/i18n';\nimport { getProjectDisplayTags } from '../utils/projectTags';\nimport type { ProjectTag, Workspace } from '../types';\n\nexport function ProjectsListPage() {\n const { t } = useI18n();\n\n // The URL path `:workspaceId` (when present) intentionally OVERRIDES the active\n // context — it scopes this page to a specific workspace for deep-links/drill-in.\n // The cross-workspace route (`/workspaces/projects`, no path param) leaves it\n // undefined so the list stays cross-workspace. The stale-path-after-context-switch\n // problem is handled by WorkspaceContextPathSync (it rewrites this segment when the\n // active context changes), so the override never points at a stale workspace.\n // (BOFF-2805 follow-up)\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.projects', { workspaceId, requireParams: true });\n const {\n viewMode,\n setViewMode,\n projectFilters,\n setProjectFilters,\n clearProjectFilters,\n selectedProjectIds,\n toggleProjectSelection,\n clearProjectSelection,\n openCreateProjectDialog,\n isCreateProjectDialogOpen,\n } = useWorkspacesStore();\n\n const [searchValue, setSearchValue] = useState(projectFilters.search || '');\n const [activeTagFilters, setActiveTagFilters] = useState<ProjectTag[]>([]);\n const [matchAllTagFilters, setMatchAllTagFilters] = useState(true);\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setProjectFilters({ search: debouncedSearch });\n }, [debouncedSearch, setProjectFilters]);\n\n useEffect(() => {\n if (activeTagFilters.length === 0) {\n setProjectFilters({\n tagIds: undefined,\n tagHierarchyPaths: undefined,\n matchAllTags: undefined,\n includeDescendants: undefined,\n });\n return;\n }\n\n setProjectFilters({\n tagIds: activeTagFilters.map((tag) => tag.tagId),\n tagHierarchyPaths: activeTagFilters\n .map((tag) => tag.hierarchyPath)\n .filter((tagPath): tagPath is string => typeof tagPath === 'string' && tagPath.length > 0),\n matchAllTags: matchAllTagFilters,\n includeDescendants: true,\n });\n }, [activeTagFilters, matchAllTagFilters, setProjectFilters]);\n\n useEffect(() => {\n return () => clearProjectSelection();\n }, [clearProjectSelection]);\n\n // Membership-scoped query: backend filters by actorId, so no client-side\n // permission gate is needed. Page is shared by `/workspaces/projects` and\n // `/workspaces/:workspaceId/projects`; the latter narrows the list below.\n const PROJECTS_PAGE_SIZE = 500;\n const {\n data: myData,\n isLoading,\n error,\n } = useMyProjects({\n limit: PROJECTS_PAGE_SIZE,\n offset: 0,\n });\n\n const data = useMemo(() => {\n const all = myData?.items ?? [];\n const q = (projectFilters.search ?? '').trim().toLowerCase();\n const tagIds = projectFilters.tagIds ?? [];\n const matchAllTags = projectFilters.matchAllTags ?? true;\n\n const matchesWorkspace = (project: (typeof all)[number]) =>\n !workspaceId || project.workspaceId === workspaceId;\n const matchesSearch = (project: (typeof all)[number]) =>\n !q || project.name.toLowerCase().includes(q) || project.key.toLowerCase().includes(q);\n const matchesStatus = (project: (typeof all)[number]) =>\n !projectFilters.status || project.status === projectFilters.status;\n const matchesArchived = (project: (typeof all)[number]) =>\n typeof projectFilters.archived === 'undefined' ||\n project.archived === projectFilters.archived;\n const matchesTags = (project: (typeof all)[number]) => {\n if (tagIds.length === 0) return true;\n const projectTagIds = new Set(getProjectDisplayTags(project).map((t) => t.tagId));\n return matchAllTags\n ? tagIds.every((id) => projectTagIds.has(id))\n : tagIds.some((id) => projectTagIds.has(id));\n };\n\n const filtered = all.filter(\n (project) =>\n matchesWorkspace(project) &&\n matchesSearch(project) &&\n matchesStatus(project) &&\n matchesArchived(project) &&\n matchesTags(project)\n );\n\n // Surface the real backend totals so the \"Showing X of Y\" footer can flag\n // truncation. For narrowed views (workspace/tag filter), the backend total\n // doesn't reflect the post-filter count, so we report the filtered size as\n // total. But we MUST preserve `hasMore` from upstream — if the upstream\n // query was capped (hasMore=true), matches outside the first page are\n // silently hidden, and the footer needs to keep flagging that.\n const apiTotal = myData?.total ?? filtered.length;\n const apiHasMore = myData?.hasMore ?? false;\n const isNarrowed = !!workspaceId || tagIds.length > 0;\n return {\n items: filtered,\n total: isNarrowed ? filtered.length : apiTotal,\n hasMore: apiHasMore,\n };\n }, [myData?.items, myData?.total, myData?.hasMore, projectFilters, workspaceId]);\n\n const { mutateAsync: archiveProject } = useArchiveProject(workspaceId);\n const { mutateAsync: unarchiveProject } = useUnarchiveProject(workspaceId);\n const { mutateAsync: deleteProject } = useDeleteProject(workspaceId);\n\n const selectedProjects = useMemo(\n () => data?.items.filter((project) => selectedProjectIds.has(project.id)) ?? [],\n [data?.items, selectedProjectIds]\n );\n\n const archivableProjects = useMemo(\n () => selectedProjects.filter((project) => !project.archived),\n [selectedProjects]\n );\n\n const unarchivableProjects = useMemo(\n () => selectedProjects.filter((project) => project.archived),\n [selectedProjects]\n );\n\n const allVisibleSelected =\n (data?.items.length ?? 0) > 0 &&\n (data?.items ?? []).every((project) => selectedProjectIds.has(project.id));\n const partiallySelected =\n (data?.items ?? []).some((project) => selectedProjectIds.has(project.id)) &&\n !allVisibleSelected;\n\n const {\n canCreateProject,\n canUpdateProject,\n canDeleteProject,\n canArchiveProject,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n );\n }\n\n // No client-side list gate: this page calls `myProjects` which is\n // membership-scoped — the resolver filters by actorId on the backend, so the\n // data set returned IS the access list. If the user is in zero projects, the\n // page renders the empty state below. Per-action checks (canCreateProject,\n // canDeleteProject, canArchiveProject) remain in place to hide buttons the\n // user isn't authorized for on their workspace.\n\n const handleCreateProject = () => {\n openCreateProjectDialog({} as Workspace);\n };\n\n const handleStatusFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.status;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ status: value as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED' });\n }\n };\n\n const handleArchivedFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.archived;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ archived: value === 'true' });\n }\n };\n\n const toggleTagFilter = (tag: ProjectTag) => {\n setActiveTagFilters((current) => {\n const exists = current.some((entry) => entry.tagId === tag.tagId);\n if (exists) {\n return current.filter((entry) => entry.tagId !== tag.tagId);\n }\n\n return [...current, tag];\n });\n };\n\n const clearAllProjectFilters = () => {\n setSearchValue('');\n setActiveTagFilters([]);\n setMatchAllTagFilters(true);\n clearProjectFilters();\n };\n\n const toggleAllProjects = (checked: boolean) => {\n (data?.items ?? []).forEach((project) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n });\n };\n\n const handleBulkArchive = async () => {\n if (archivableProjects.length === 0) return;\n await Promise.allSettled(archivableProjects.map((project) => archiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkUnarchive = async () => {\n if (unarchivableProjects.length === 0) return;\n await Promise.allSettled(unarchivableProjects.map((project) => unarchiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkDelete = async () => {\n if (selectedProjects.length === 0) return;\n void nativeImpact('medium');\n const message =\n tWithFallback(t, 'projectsList.bulkDeleteConfirmPrefix', 'Delete ') +\n selectedProjects.length +\n ' ' +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmProject', 'selected project') +\n (selectedProjects.length === 1\n ? ''\n : tWithFallback(t, 'projectsList.bulkDeleteConfirmPluralSuffix', 's')) +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmSuffix', '? This cannot be undone.');\n const native = await nativeConfirm({\n title: tWithFallback(t, 'projectsList.deleteProjectsTitle', 'Delete projects'),\n message,\n okButtonTitle: tWithFallback(t, 'common.delete', 'Delete'),\n cancelButtonTitle: tWithFallback(t, 'common.cancel', 'Cancel'),\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n await Promise.allSettled(selectedProjects.map((project) => deleteProject(project.id)));\n clearProjectSelection();\n };\n\n return (\n <PageContainer padded={false}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'projectsList.pageTitle', 'Projects')}\n </h1>\n <p className=\"text-text-secondary\">\n {tWithFallback(t, 'projectsList.pageSubtitle', 'Manage and organize your projects')}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* View mode toggle */}\n <div className=\"flex items-center gap-1 border rounded-lg p-1\">\n <Button\n variant={viewMode === 'grid' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('grid')}\n className=\"size-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.gridViewAriaLabel',\n 'Show projects in grid view'\n )}\n >\n <Grid3x3 size={16} />\n </Button>\n <Button\n variant={viewMode === 'list' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('list')}\n className=\"size-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.listViewAriaLabel',\n 'Show projects in list view'\n )}\n >\n <List size={16} />\n </Button>\n </div>\n {canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus className=\"size-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n <PagePurpose>\n {tWithFallback(\n t,\n 'projectsList.purpose',\n 'Projects are the individual streams of work inside your workspaces — a launch, a client engagement, a feature. Each carries its own members, tags and activity. Use search, status and tag filters to find the one you need, or create a new project to start tracking work.'\n )}\n </PagePurpose>\n\n {/* Filters & Search */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"relative flex-1\">\n <Search\n className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-text-secondary\"\n size={18}\n />\n <Input\n placeholder={tWithFallback(t, 'projectsList.searchPlaceholder', 'Search projects...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-[3.25rem]\"\n />\n </div>\n <Select value={projectFilters.status || 'all'} onValueChange={handleStatusFilter}>\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByStatus', 'Filter by status')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.statusAll', 'All Statuses')}\n </SelectItem>\n <SelectItem value=\"ACTIVE\">\n {tWithFallback(t, 'projectsList.statusActive', 'Active')}\n </SelectItem>\n <SelectItem value=\"INACTIVE\">\n {tWithFallback(t, 'projectsList.statusInactive', 'Inactive')}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select\n value={\n projectFilters.archived === undefined\n ? 'all'\n : projectFilters.archived\n ? 'true'\n : 'false'\n }\n onValueChange={handleArchivedFilter}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByArchive', 'Filter by archive')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.archiveAll', 'All Projects')}\n </SelectItem>\n <SelectItem value=\"false\">\n {tWithFallback(t, 'projectsList.archiveActiveProjects', 'Active Projects')}\n </SelectItem>\n <SelectItem value=\"true\">\n {tWithFallback(t, 'projectsList.archiveArchived', 'Archived')}\n </SelectItem>\n </SelectContent>\n </Select>\n {(projectFilters.search ||\n projectFilters.status ||\n projectFilters.archived !== undefined ||\n activeTagFilters.length > 0) && (\n <Button variant=\"outline\" onClick={clearAllProjectFilters}>\n {tWithFallback(t, 'projectsList.clearFilters', 'Clear Filters')}\n </Button>\n )}\n </div>\n\n {activeTagFilters.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface px-4 py-3\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-medium\">\n {tWithFallback(t, 'projectsList.activeTagFilters', 'Active tag filters')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tWithFallback(\n t,\n 'projectsList.activeTagFiltersDescription',\n 'Click a project tag to add or remove it from the current filter set.'\n )}\n </p>\n </div>\n <Select\n value={matchAllTagFilters ? 'all' : 'any'}\n onValueChange={(value) => setMatchAllTagFilters(value === 'all')}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.tagMatchMode', 'Tag match mode')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.tagMatchAll', 'Match all tags')}\n </SelectItem>\n <SelectItem value=\"any\">\n {tWithFallback(t, 'projectsList.tagMatchAny', 'Match any tag')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {activeTagFilters.map((tag) => (\n <TagBadge\n key={tag.tagId}\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => toggleTagFilter(tag)}\n size=\"sm\"\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Error State */}\n {error && (\n <GlassCard treatment=\"plain\" className=\"border-status-error-border\">\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-status-error-text font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.errorLoading', 'Error loading projects')}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'projectsList.unknownError', 'An unknown error occurred')}\n </p>\n </div>\n </CardContent>\n </GlassCard>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <Card key={i} className=\"border-border-seam\">\n <CardContent className=\"pt-6\">\n <div className=\"space-y-3\">\n <Skeleton className=\"size-12 rounded-lg\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && data?.items.length === 0 && (\n <GlassCard treatment=\"plain\" className=\"border-border-seam\">\n <CardContent className=\"pt-6\">\n <IllustratedEmptyState\n illustration={projectFilters.search ? 'empty-search' : 'empty-data'}\n title={tWithFallback(t, 'projectsList.noProjectsFound', 'No projects found')}\n description={\n projectFilters.search\n ? tWithFallback(\n t,\n 'projectsList.adjustSearchOrFilters',\n 'Try adjusting your search or filters'\n )\n : tWithFallback(\n t,\n 'projectsList.createFirstProject',\n 'Create your first project to get started organizing your work'\n )\n }\n action={\n !projectFilters.search && canCreateProject ? (\n <Button onClick={handleCreateProject}>\n <Plus size={16} className=\"mr-2\" />\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </Button>\n ) : undefined\n }\n />\n </CardContent>\n </GlassCard>\n )}\n\n {/* Projects Grid */}\n {!isLoading && !error && data && data.items.length > 0 && (\n <>\n <div className=\"flex flex-col gap-3 rounded-lg border border-border-seam bg-bg-surface p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllProjects(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'projectsList.selectAllAriaLabel',\n 'Select all projects'\n )}\n className=\"size-4 rounded border-border-subtle text-primary\"\n />\n <span>\n {tWithFallback(t, 'projectsList.selectAllVisible', 'Select all visible')}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {selectedProjectIds.size} {tWithFallback(t, 'projectsList.selected', 'selected')}\n </span>\n {selectedProjectIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearProjectSelection}>\n {tWithFallback(t, 'projectsList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {/* Bulk mutations are bound to a single workspaceId via the\n archive/unarchive/delete hooks. On the cross-workspace\n /workspaces/projects route (workspaceId is undefined),\n hide them — selections can span workspaces and the bound\n hook would fail authorization for projects outside the\n caller's context. The per-card actions in ProjectCard\n still work because they resolve per-project. */}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkArchive}\n disabled={archivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.archiveSelected', 'Archive Selected')}\n {archivableProjects.length > 0 ? ` (${archivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkUnarchive}\n disabled={unarchivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.unarchiveSelected', 'Unarchive Selected')}\n {unarchivableProjects.length > 0 ? ` (${unarchivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canDeleteProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkDelete}\n disabled={selectedProjects.length === 0}\n className=\"text-status-error-text\"\n >\n {tWithFallback(t, 'projectsList.deleteSelected', 'Delete Selected')}\n {selectedProjects.length > 0 ? ` (${selectedProjects.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n\n <div\n className={\n viewMode === 'list'\n ? 'space-y-3'\n : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'\n }\n >\n {data.items.map((project) => (\n <ProjectCard\n key={project.id}\n project={project}\n selected={selectedProjectIds.has(project.id)}\n canArchive={canArchiveProject}\n canDelete={canDeleteProject}\n canRename={canUpdateProject}\n onTagClick={toggleTagFilter}\n onToggleSelect={(checked) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n }}\n />\n ))}\n </div>\n\n {/* Pagination Info */}\n {data.total > data.items.length && (\n <div className=\"text-center text-sm text-text-secondary\">\n {tWithFallback(t, 'projectsList.showingPrefix', 'Showing')} {data.items.length}{' '}\n {tWithFallback(t, 'projectsList.showingOf', 'of')} {data.total}{' '}\n {tWithFallback(t, 'projectsList.showingProjects', 'projects')}\n </div>\n )}\n </>\n )}\n\n {/* Create Project Dialog */}\n {isCreateProjectDialogOpen && <CreateProjectDialog workspaceId={workspaceId} />}\n </div>\n </PageContainer>\n );\n}\n"],"mappings":"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"}
|
|
@@ -547,7 +547,7 @@ function k() {
|
|
|
547
547
|
}), /* @__PURE__ */ m(Oe, {
|
|
548
548
|
id: "workspace-invite-email",
|
|
549
549
|
type: "email",
|
|
550
|
-
placeholder: "member@example.com",
|
|
550
|
+
placeholder: r(x, "workspaceInvite.emailPlaceholder", "member@example.com"),
|
|
551
551
|
value: P,
|
|
552
552
|
onChange: (e) => ot(e.target.value),
|
|
553
553
|
disabled: q
|
|
@@ -653,7 +653,7 @@ function k() {
|
|
|
653
653
|
t && Mt(t), e.target.value = "";
|
|
654
654
|
},
|
|
655
655
|
className: "hidden",
|
|
656
|
-
"aria-label": "Choose CSV file"
|
|
656
|
+
"aria-label": r(x, "workspaceInvite.chooseCsvFileAriaLabel", "Choose CSV file")
|
|
657
657
|
}),
|
|
658
658
|
/* @__PURE__ */ m(_, {
|
|
659
659
|
type: "button",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspaceInvitePage.js","names":[],"sources":["../../src/pages/WorkspaceInvitePage.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n AlertCircle,\n ArrowLeft,\n CheckCircle2,\n Download,\n Loader2,\n Mail,\n RefreshCw,\n Send,\n Upload,\n UserPlus,\n XCircle,\n} from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Badge,\n Button,\n GlassCard,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Input,\n Skeleton,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n ResponsiveTable,\n PagePurpose,\n NextSteps,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport type { ResponsiveTableColumn } from '@burdenoff/fe-libs/ui';\nimport { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\nimport { getProjectDisplayNames, getRoleDisplayNames } from '../utils/invitationDisplay';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspace } from '../hooks/useWorkspaces';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspaceRoles } from '../hooks/useWorkspaceRoles';\nimport { useProjects } from '../hooks/useProjects';\nimport { useWorkspaceInvitations } from '../hooks/useWorkspaceMembers';\nimport {\n useBulkCreateInvitations,\n useCancelInvitation,\n useCreateInvitation,\n useResendInvitation,\n} from '../hooks/useMemberMutations';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport type { Project, WorkspaceInvitation } from '../types';\nimport {\n downloadWorkspaceBulkInviteTemplate,\n parseWorkspaceBulkInviteCsv,\n type WorkspaceBulkInviteRow,\n} from '../utils/workspaceBulkInviteCsv';\n\ntype InviteMode = 'single' | 'bulk';\n\ntype RowStatus =\n | { kind: 'idle' }\n | { kind: 'sending' }\n | { kind: 'invited' }\n | { kind: 'failed'; message: string };\n\ninterface PreviewRow extends WorkspaceBulkInviteRow {\n status: RowStatus;\n}\n\ntype FeedbackState = {\n tone: 'success' | 'error';\n message: string;\n} | null;\n\nconst MAX_BULK_INVITE_FILE_SIZE = 1024 * 1024;\nconst MAX_BULK_INVITE_ROWS = 500;\n\nconst previewColumns: ResponsiveTableColumn<PreviewRow>[] = [\n {\n key: 'rowNumber',\n header: '#',\n priority: 'secondary',\n headerClassName: 'text-xs font-semibold uppercase tracking-wide text-text-secondary',\n cellClassName: 'text-xs text-text-secondary tabular-nums',\n cell: (row) => row.rowNumber,\n },\n {\n key: 'email',\n header: 'Email',\n priority: 'primary',\n headerClassName: 'text-xs font-semibold uppercase tracking-wide text-text-secondary',\n cellClassName: 'text-sm',\n cell: (row) => row.email || <span className=\"italic text-text-secondary\">(empty)</span>,\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'secondary',\n headerClassName: 'text-xs font-semibold uppercase tracking-wide text-text-secondary',\n cellClassName: 'text-xs',\n cell: (row) => {\n const hasParseErrors = row.errors.length > 0;\n if (hasParseErrors) {\n return (\n <div className=\"space-y-1 text-status-error-text\">\n {row.errors.map((errorText) => (\n <span\n key={`${row.rowNumber}-${errorText}`}\n className=\"inline-flex items-center gap-1 pr-3\"\n >\n <XCircle size={14} />\n {errorText}\n </span>\n ))}\n </div>\n );\n }\n if (row.status.kind === 'sending') {\n return (\n <span className=\"inline-flex items-center gap-1 text-text-secondary\">\n <Loader2 size={14} className=\"animate-spin\" />\n Sending...\n </span>\n );\n }\n if (row.status.kind === 'invited') {\n return (\n <span className=\"inline-flex items-center gap-1 text-green-700 dark:text-green-400\">\n <CheckCircle2 size={14} />\n Invited\n </span>\n );\n }\n if (row.status.kind === 'failed') {\n return (\n <span className=\"inline-flex items-center gap-1 text-status-error-text\">\n <XCircle size={14} />\n {row.status.message}\n </span>\n );\n }\n return <span className=\"text-text-secondary\">Ready</span>;\n },\n },\n];\n\nfunction isCsvFile(file: File): boolean {\n return file.type === 'text/csv' || file.name.toLowerCase().endsWith('.csv');\n}\n\nfunction formatDate(value?: string) {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n\n return date.toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nfunction formatInviteError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('CONFLICT')) {\n return 'An invite already exists for this email address.';\n }\n if (message.includes('already a member')) {\n return 'This user is already a member of the workspace.';\n }\n return 'Something went wrong. Please try again or contact support.';\n}\n\nfunction formatSelectionSummary(\n items: string[],\n emptyLabel: string,\n formatter?: (value: string) => string\n) {\n if (items.length === 0) {\n return emptyLabel;\n }\n\n return items.map((item) => (formatter ? formatter(item) : item)).join(', ');\n}\n\nfunction buildInvitationPayload(\n workspaceId: string,\n email: string,\n roleIds: string[],\n projectIds: string[]\n) {\n return {\n email,\n workspaceId,\n roleIds,\n ...(projectIds.length > 0\n ? {\n context: {\n type: 'workspace.invitation',\n metadata: { projectIds },\n },\n }\n : {}),\n };\n}\n\nfunction WorkspaceSelectorSection({\n selectedRoleIds,\n onRoleToggle,\n selectedProjectIds,\n onProjectToggle,\n availableRoles,\n availableProjects,\n isLoadingRoles,\n isLoadingProjects,\n disabled,\n}: {\n selectedRoleIds: string[];\n onRoleToggle: (roleId: string, checked: boolean) => void;\n selectedProjectIds: string[];\n onProjectToggle: (projectId: string, checked: boolean) => void;\n availableRoles: Array<{\n id: string;\n name: string;\n description?: string | null;\n isSystem: boolean;\n }>;\n availableProjects: Project[];\n isLoadingRoles: boolean;\n isLoadingProjects: boolean;\n disabled: boolean;\n}) {\n return (\n <div className=\"space-y-6\">\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">Roles (Optional)</h3>\n <p className=\"text-sm text-text-secondary\">\n Choose roles. Hover any role to preview its full description.\n </p>\n </div>\n {selectedRoleIds.length > 0 ? (\n <Badge variant=\"secondary\">{selectedRoleIds.length} selected</Badge>\n ) : null}\n </div>\n <TooltipProvider>\n <div className=\"grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3\">\n {isLoadingRoles ? (\n <p className=\"text-sm text-text-secondary\">Loading roles…</p>\n ) : availableRoles.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No assignable roles were returned for this workspace.\n </p>\n ) : (\n availableRoles.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id);\n return (\n <Tooltip key={role.id}>\n <TooltipTrigger asChild>\n <label\n className={`flex cursor-pointer items-center justify-between gap-3 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors focus-within:ring-2 focus-within:ring-[var(--color-focus-ring)] focus-within:ring-offset-2 ${\n isSelected\n ? 'border-primary bg-action-primary-bg/10 text-text-primary shadow-sm'\n : 'border-border-subtle bg-bg-surface text-text-primary hover:bg-accent'\n } ${disabled ? 'opacity-70' : ''}`}\n >\n <span className=\"min-w-0 flex-1 truncate\">{role.name}</span>\n <Checkbox\n checked={isSelected}\n disabled={disabled}\n onCheckedChange={(checked) => onRoleToggle(role.id, Boolean(checked))}\n aria-label={`Select ${role.name}`}\n />\n </label>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs text-sm leading-6\">\n <p className=\"font-medium\">{role.name}</p>\n <p className=\"mt-1 text-text-secondary\">\n {role.description || 'No description available for this role.'}\n </p>\n {role.isSystem ? <p className=\"mt-2 text-xs\">System role</p> : null}\n </TooltipContent>\n </Tooltip>\n );\n })\n )}\n </div>\n </TooltipProvider>\n </div>\n\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">Projects to add on acceptance (Optional)</h3>\n <p className=\"text-sm text-text-secondary\">\n The invited user will be added to these projects automatically after they accept.\n </p>\n </div>\n {selectedProjectIds.length > 0 ? (\n <Badge variant=\"secondary\">{selectedProjectIds.length} selected</Badge>\n ) : null}\n </div>\n <div className=\"grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3\">\n {isLoadingProjects ? (\n <p className=\"text-sm text-text-secondary\">Loading projects…</p>\n ) : availableProjects.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No active projects are available in this workspace yet.\n </p>\n ) : (\n availableProjects.map((project) => {\n const isSelected = selectedProjectIds.includes(project.id);\n return (\n <label\n key={project.id}\n className={`flex cursor-pointer items-start justify-between gap-3 rounded-lg border px-3 py-3 transition-colors focus-within:ring-2 focus-within:ring-[var(--color-focus-ring)] focus-within:ring-offset-2 ${\n isSelected\n ? 'border-primary bg-action-primary-bg/10 text-text-primary shadow-sm'\n : 'border-border-subtle bg-bg-surface text-text-primary hover:bg-accent'\n } ${disabled ? 'opacity-70' : ''}`}\n >\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"truncate text-sm font-semibold leading-tight\">{project.name}</p>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <span className=\"truncate font-mono\">{project.key}</span>\n <Badge variant=\"outline\">{project.status}</Badge>\n </div>\n </div>\n <Checkbox\n checked={isSelected}\n disabled={disabled}\n onCheckedChange={(checked) => onProjectToggle(project.id, Boolean(checked))}\n aria-label={`Select ${project.name}`}\n />\n </label>\n );\n })\n )}\n </div>\n </div>\n </div>\n );\n}\n\nfunction WorkspaceInvitePageContent() {\n const { workspaceId: routeWorkspaceId } = useParams<{ workspaceId: string }>();\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/workspaces', workspaceId: contextWorkspaceId } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const workspaceId = routeWorkspaceId || contextWorkspaceId || '';\n\n useWorkspacesPageView('workspaces.invite', { workspaceId: workspaceId || undefined });\n\n const { canInviteMember, isLoading: permissionsLoading } = useWorkspacePermissions();\n const {\n data: workspace,\n isLoading: isLoadingWorkspace,\n error: workspaceError,\n } = useWorkspace(workspaceId);\n const { data: availableRoles = [], isLoading: isLoadingRoles } = useWorkspaceRoles(workspaceId);\n const { data: projectList, isLoading: isLoadingProjects } = useProjects(\n { archived: false },\n { limit: 100 },\n workspaceId\n );\n const {\n data: invitationsData,\n isLoading: isLoadingInvitations,\n isFetching: isFetchingInvitations,\n error: invitationsError,\n refetch: refetchInvitations,\n } = useWorkspaceInvitations(workspaceId, 'PENDING', { limit: 50 }, { retry: false });\n const availableProjects = useMemo(\n () => (projectList?.items ?? []).filter((project) => !project.archived),\n [projectList?.items]\n );\n\n const [mode, setMode] = useState<InviteMode>('single');\n const [email, setEmail] = useState('');\n const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);\n const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);\n const [singleError, setSingleError] = useState<string | null>(null);\n const [singleSuccess, setSingleSuccess] = useState<string | null>(null);\n const [bulkError, setBulkError] = useState<string | null>(null);\n const [bulkSuccess, setBulkSuccess] = useState<string | null>(null);\n const [pendingFeedback, setPendingFeedback] = useState<FeedbackState>(null);\n const [rows, setRows] = useState<PreviewRow[]>([]);\n const [fileName, setFileName] = useState<string | null>(null);\n const [dragActive, setDragActive] = useState(false);\n const [isSubmittingBulk, setIsSubmittingBulk] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [cancelTarget, setCancelTarget] = useState<WorkspaceInvitation | null>(null);\n\n const { mutateAsync: createInvitation, isPending: isCreatingInvitation } =\n useCreateInvitation(workspaceId);\n const { mutateAsync: bulkCreateInvitations, isPending: isCreatingBulkInvites } =\n useBulkCreateInvitations();\n const { mutate: resendInvitation, isPending: isResendingInvitation } =\n useResendInvitation(workspaceId);\n const { mutate: cancelInvitation, isPending: isCancellingInvitation } =\n useCancelInvitation(workspaceId);\n const [invitationActionId, setInvitationActionId] = useState<string | null>(null);\n\n const pendingInvitations = invitationsData?.items ?? [];\n const isMutatingInvite = isCreatingInvitation || isCreatingBulkInvites || isSubmittingBulk;\n const selectedRoleNames = useMemo(\n () =>\n availableRoles.filter((role) => selectedRoleIds.includes(role.id)).map((role) => role.name),\n [availableRoles, selectedRoleIds]\n );\n const selectedProjectNames = useMemo(\n () =>\n availableProjects\n .filter((project) => selectedProjectIds.includes(project.id))\n .map((project) => project.name),\n [availableProjects, selectedProjectIds]\n );\n\n const readyRows = useMemo(\n () => rows.filter((row) => row.errors.length === 0 && row.status.kind !== 'invited'),\n [rows]\n );\n const invalidRows = useMemo(() => rows.filter((row) => row.errors.length > 0), [rows]);\n\n if (!workspaceId) {\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"rounded-xl border bg-bg-surface p-8 text-center\">\n <h1 className=\"text-2xl font-bold\">\n {tWithFallback(t, 'workspaceInvite.noWorkspaceTitle', 'Select a workspace first')}\n </h1>\n <p className=\"mt-2 text-text-secondary\">\n {tWithFallback(\n t,\n 'workspaceInvite.noWorkspaceDescription',\n 'Choose a current workspace from the header or open a workspace detail page before inviting members.'\n )}\n </p>\n <div className=\"mt-4\">\n <Button\n onClick={() => navigate(withCurrentSearch(`${basePath}/list`))}\n variant=\"outline\"\n >\n {tWithFallback(t, 'workspaceInvite.backToWorkspaces', 'Back to Workspaces')}\n </Button>\n </div>\n </div>\n </div>\n );\n }\n\n if (permissionsLoading || isLoadingWorkspace) {\n return (\n <div className=\"space-y-6 p-6\">\n <Skeleton className=\"h-10 w-48\" />\n <Skeleton className=\"h-40 w-full\" />\n <Skeleton className=\"h-80 w-full\" />\n </div>\n );\n }\n\n if (!canInviteMember) {\n return (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <div className=\"space-y-2 text-center\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'workspaceInvite.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'workspaceInvite.accessDeniedDescription',\n \"You don't have permission to invite members into this workspace.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n if (workspaceError || !workspace) {\n return (\n <div className=\"space-y-4 p-6\">\n <Button\n variant=\"ghost\"\n onClick={() => navigate(withCurrentSearch(`${basePath}/list`))}\n className=\"gap-2\"\n >\n <ArrowLeft size={18} />\n {tWithFallback(t, 'workspaceInvite.backToWorkspaces', 'Back to Workspaces')}\n </Button>\n <div className=\"rounded-xl border bg-bg-surface p-8 text-center text-status-error-text\">\n {tWithFallback(\n t,\n 'workspaceInvite.workspaceLoadError',\n 'Unable to load workspace invite details.'\n )}\n </div>\n </div>\n );\n }\n\n const handleRoleToggle = (roleId: string, checked: boolean) => {\n setSelectedRoleIds((prev) =>\n checked\n ? [...prev, roleId].filter((value, index, array) => array.indexOf(value) === index)\n : prev.filter((id) => id !== roleId)\n );\n };\n\n const handleProjectToggle = (projectId: string, checked: boolean) => {\n setSelectedProjectIds((prev) =>\n checked\n ? [...prev, projectId].filter((value, index, array) => array.indexOf(value) === index)\n : prev.filter((id) => id !== projectId)\n );\n };\n\n const handleSingleInvite = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setSingleError(null);\n setSingleSuccess(null);\n setPendingFeedback(null);\n\n if (!email.trim()) {\n setSingleError('Email is required.');\n return;\n }\n\n try {\n await createInvitation(\n buildInvitationPayload(\n workspaceId,\n email.trim().toLowerCase(),\n selectedRoleIds,\n selectedProjectIds\n )\n );\n setEmail('');\n setSingleSuccess(`Invitation sent to ${email.trim().toLowerCase()}.`);\n } catch (error) {\n setSingleError(formatInviteError(error));\n }\n };\n\n const handleFile = (file: File) => {\n if (!isCsvFile(file)) {\n setBulkError('Please upload a .csv file.');\n return;\n }\n\n if (file.size > MAX_BULK_INVITE_FILE_SIZE) {\n setBulkError('CSV file is too large (max 1 MB).');\n return;\n }\n\n const reader = new FileReader();\n reader.onload = () => {\n const text = typeof reader.result === 'string' ? reader.result : '';\n const parsed = parseWorkspaceBulkInviteCsv(text);\n const limitedRows = parsed.slice(0, MAX_BULK_INVITE_ROWS);\n setRows(limitedRows.map((row) => ({ ...row, status: { kind: 'idle' } })));\n setFileName(file.name);\n setBulkError(\n parsed.length > MAX_BULK_INVITE_ROWS\n ? `Only the first ${MAX_BULK_INVITE_ROWS} rows were loaded. Split larger CSV files into smaller batches.`\n : null\n );\n setBulkSuccess(null);\n setPendingFeedback(null);\n };\n reader.onerror = () => {\n setBulkError('Failed to read CSV file.');\n };\n reader.readAsText(file);\n };\n\n const handleBulkInvite = async () => {\n if (readyRows.length === 0 || isSubmittingBulk) {\n return;\n }\n\n setBulkError(null);\n setBulkSuccess(null);\n setPendingFeedback(null);\n setIsSubmittingBulk(true);\n setRows((prev) =>\n prev.map((row) =>\n row.errors.length === 0 && row.status.kind !== 'invited'\n ? { ...row, status: { kind: 'sending' } }\n : row\n )\n );\n\n try {\n await bulkCreateInvitations(\n readyRows.map((row) =>\n buildInvitationPayload(workspaceId, row.email, selectedRoleIds, selectedProjectIds)\n )\n );\n\n setRows((prev) =>\n prev.map((row) =>\n row.status.kind === 'sending' ? { ...row, status: { kind: 'invited' } } : row\n )\n );\n setBulkSuccess(\n `Sent ${readyRows.length} workspace invitation${readyRows.length === 1 ? '' : 's'}.`\n );\n } catch (error) {\n const message = formatInviteError(error);\n setRows((prev) =>\n prev.map((row) =>\n row.status.kind === 'sending' ? { ...row, status: { kind: 'failed', message } } : row\n )\n );\n setBulkError(message);\n } finally {\n setIsSubmittingBulk(false);\n }\n };\n\n const handleResendInvitation = (invitation: WorkspaceInvitation) => {\n setPendingFeedback(null);\n setInvitationActionId(invitation.id);\n resendInvitation(invitation.id, {\n onSuccess: () => {\n setPendingFeedback({\n tone: 'success',\n message: `Invitation resent to ${invitation.email}.`,\n });\n },\n onError: (error) => {\n setPendingFeedback({ tone: 'error', message: formatInviteError(error) });\n },\n onSettled: () => setInvitationActionId(null),\n });\n };\n\n const handleConfirmCancel = () => {\n if (!cancelTarget) {\n return;\n }\n\n setPendingFeedback(null);\n setInvitationActionId(cancelTarget.id);\n cancelInvitation(cancelTarget.id, {\n onSuccess: () => {\n setPendingFeedback({\n tone: 'success',\n message: `Invitation cancelled for ${cancelTarget.email}.`,\n });\n setCancelTarget(null);\n },\n onError: (error) => {\n setPendingFeedback({ tone: 'error', message: formatInviteError(error) });\n },\n onSettled: () => setInvitationActionId(null),\n });\n };\n\n return (\n <>\n <AlertDialog\n open={Boolean(cancelTarget)}\n onOpenChange={(open) => !open && setCancelTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Cancel pending invitation?</AlertDialogTitle>\n <AlertDialogDescription>\n {cancelTarget\n ? `This will revoke the pending invite for ${cancelTarget.email}. They will need a new invitation to join later.`\n : 'This will revoke the pending invitation.'}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={isCancellingInvitation}>Keep invitation</AlertDialogCancel>\n <AlertDialogAction onClick={handleConfirmCancel} disabled={isCancellingInvitation}>\n {isCancellingInvitation ? <Loader2 className=\"size-4 animate-spin\" /> : null}\n Cancel invitation\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <div className=\"space-y-6 p-3 sm:p-6\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <Button\n variant=\"ghost\"\n onClick={() => navigate(withCurrentSearch(`${basePath}/${workspaceId}`))}\n className=\"-ml-2 mb-2 gap-2\"\n >\n <ArrowLeft size={18} />\n {tWithFallback(t, 'workspaceInvite.backToWorkspace', 'Back to Workspace')}\n </Button>\n <div className=\"flex items-center gap-2\">\n <UserPlus className=\"text-primary\" size={24} />\n <h1 className=\"text-2xl font-bold sm:text-3xl\">\n {tWithFallback(t, 'workspaceInvite.title', 'Invite Members')}\n </h1>\n </div>\n <p className=\"mt-2 text-sm text-text-secondary sm:text-base\">\n {workspace.name} —{' '}\n {tWithFallback(\n t,\n 'workspaceInvite.subtitle',\n 'Invite people into this workspace and optionally grant project access.'\n )}\n </p>\n </div>\n </div>\n\n <PagePurpose>\n {tWithFallback(\n t,\n 'workspaceInvite.purpose',\n 'Use this page to bring new people into the workspace. First choose the roles and projects they should get the moment they accept, then send a single invitation by email or upload a CSV to invite many people at once. Invitations you have already sent appear at the bottom, where you can resend or cancel them.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"workspaces-workspace-invite\"\n steps={[\n {\n id: 'pick-access',\n label: tWithFallback(\n t,\n 'workspaceInvite.stepAccess',\n 'Pick the roles and projects to grant'\n ),\n description: tWithFallback(\n t,\n 'workspaceInvite.stepAccessDesc',\n 'These are applied automatically when each person accepts.'\n ),\n icon: <UserPlus size={16} />,\n onClick: () =>\n document\n .getElementById('invite-access-section')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n {\n id: 'send-invite',\n label: tWithFallback(t, 'workspaceInvite.stepSend', 'Send the invitation'),\n description: tWithFallback(\n t,\n 'workspaceInvite.stepSendDesc',\n 'Enter one email, or switch to the Bulk CSV tab to invite many at once.'\n ),\n icon: <Send size={16} />,\n onClick: () =>\n document\n .getElementById('invite-send-section')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n {\n id: 'review-pending',\n label: tWithFallback(\n t,\n 'workspaceInvite.stepPending',\n 'Track your pending invitations'\n ),\n description: tWithFallback(\n t,\n 'workspaceInvite.stepPendingDesc',\n 'Resend or cancel invitations that have not been accepted yet.'\n ),\n icon: <Mail size={16} />,\n onClick: () =>\n document\n .getElementById('invite-pending-section')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n ]}\n />\n\n <GlassCard treatment=\"plain\" id=\"invite-access-section\" className=\"border-border-seam\">\n <CardHeader>\n <CardTitle>\n {tWithFallback(t, 'workspaceInvite.setupTitle', 'Access to grant on acceptance')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <WorkspaceSelectorSection\n selectedRoleIds={selectedRoleIds}\n onRoleToggle={handleRoleToggle}\n selectedProjectIds={selectedProjectIds}\n onProjectToggle={handleProjectToggle}\n availableRoles={availableRoles}\n availableProjects={availableProjects}\n isLoadingRoles={isLoadingRoles}\n isLoadingProjects={isLoadingProjects}\n disabled={isMutatingInvite}\n />\n </CardContent>\n </GlassCard>\n\n {/* The one emphasized zone: live selection summary */}\n <GlassCard treatment=\"metric\" glow className=\"border-border-seam\">\n <CardHeader>\n <CardTitle>Selection summary</CardTitle>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2 rounded-lg border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"text-sm font-medium\">Roles</p>\n <Badge variant=\"secondary\" className=\"tabular-nums\">\n {selectedRoleNames.length}\n </Badge>\n </div>\n <p className=\"text-sm text-text-muted\">\n {formatSelectionSummary(selectedRoleNames, 'No roles selected')}\n </p>\n </div>\n <div className=\"space-y-2 rounded-lg border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"text-sm font-medium\">Projects</p>\n <Badge variant=\"secondary\" className=\"tabular-nums\">\n {selectedProjectNames.length}\n </Badge>\n </div>\n <p className=\"text-sm text-text-muted\">\n {formatSelectionSummary(\n selectedProjectNames,\n 'No project access will be granted automatically'\n )}\n </p>\n </div>\n </CardContent>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" id=\"invite-send-section\" className=\"border-border-seam\">\n <CardHeader>\n <CardTitle>\n {tWithFallback(t, 'workspaceInvite.sendTitle', 'Send invitations')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Tabs\n value={mode}\n onValueChange={(value) => setMode(value as InviteMode)}\n className=\"space-y-5\"\n >\n <TabsList>\n <TabsTrigger value=\"single\">\n {tWithFallback(t, 'workspaceInvite.modeSingle', 'Single')}\n </TabsTrigger>\n <TabsTrigger value=\"bulk\">\n {tWithFallback(t, 'workspaceInvite.modeBulk', 'Bulk CSV')}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"single\" className=\"space-y-4\">\n {singleError ? (\n <div className=\"flex items-center gap-2 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 shrink-0\" />\n <span>{singleError}</span>\n </div>\n ) : null}\n {singleSuccess ? (\n <div className=\"flex items-center gap-2 rounded-md border border-green-500/20 bg-green-500/10 p-3 text-sm text-green-700 dark:text-green-400\">\n <CheckCircle2 className=\"size-4 shrink-0\" />\n <span>{singleSuccess}</span>\n </div>\n ) : null}\n\n <form onSubmit={handleSingleInvite} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-invite-email\" className=\"text-sm font-medium\">\n {tWithFallback(t, 'workspaceInvite.emailLabel', 'Email Address')}\n </label>\n <Input\n id=\"workspace-invite-email\"\n type=\"email\"\n placeholder=\"member@example.com\"\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n disabled={isCreatingInvitation}\n />\n </div>\n\n <div className=\"flex justify-end\">\n <Button type=\"submit\" disabled={isCreatingInvitation} className=\"gap-2\">\n {isCreatingInvitation ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Send className=\"size-4\" />\n )}\n {tWithFallback(t, 'workspaceInvite.sendSingleButton', 'Send Invitation')}\n </Button>\n </div>\n </form>\n </TabsContent>\n\n <TabsContent value=\"bulk\" className=\"space-y-5\">\n {bulkError ? (\n <div className=\"flex items-center gap-2 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 shrink-0\" />\n <span>{bulkError}</span>\n </div>\n ) : null}\n {bulkSuccess ? (\n <div className=\"flex items-center gap-2 rounded-md border border-green-500/20 bg-green-500/10 p-3 text-sm text-green-700 dark:text-green-400\">\n <CheckCircle2 className=\"size-4 shrink-0\" />\n <span>{bulkSuccess}</span>\n </div>\n ) : null}\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"space-y-1\">\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'workspaceInvite.bulkDescription',\n 'Upload a CSV with one email column. All valid rows will receive the currently selected roles and projects.'\n )}\n </p>\n <div className=\"rounded-md border bg-bg-sunken/30 px-3 py-2 text-xs text-text-secondary\">\n <p className=\"font-medium text-text-primary\">Sample format</p>\n <pre className=\"mt-1 whitespace-pre-wrap\">\n email{`\\n`}alice@example.com{`\\n`}bob@example.com\n </pre>\n </div>\n {(projectList?.total ?? 0) > availableProjects.length ? (\n <p className=\"text-xs text-text-secondary\">\n Showing the first {availableProjects.length} active projects. Narrow the\n workspace project list if you need to invite into more.\n </p>\n ) : null}\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={downloadWorkspaceBulkInviteTemplate}\n className=\"gap-2\"\n >\n <Download size={16} />\n {tWithFallback(t, 'workspaceInvite.downloadTemplate', 'Download template')}\n </Button>\n </div>\n\n <div\n onDragOver={(event) => {\n event.preventDefault();\n setDragActive(true);\n }}\n onDragLeave={() => setDragActive(false)}\n onDrop={(event) => {\n event.preventDefault();\n setDragActive(false);\n const file = event.dataTransfer.files?.[0];\n if (file) {\n handleFile(file);\n } else {\n setBulkError('Please upload a .csv file.');\n }\n }}\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-primary bg-action-primary-bg/5'\n : 'border-border-subtle bg-bg-surface'\n }`}\n >\n <Upload size={28} className=\"text-text-secondary\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium\">\n {fileName || 'Drop a CSV file here or click to browse'}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n CSV files only. Required column: email.\n </p>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,text/csv\"\n onChange={(event) => {\n const file = event.target.files?.[0];\n if (file) {\n handleFile(file);\n }\n event.target.value = '';\n }}\n className=\"hidden\"\n aria-label=\"Choose CSV file\"\n />\n <Button type=\"button\" onClick={() => fileInputRef.current?.click()}>\n {tWithFallback(t, 'workspaceInvite.chooseFile', 'Choose file')}\n </Button>\n </div>\n\n {rows.length > 0 ? (\n <>\n <div className=\"flex flex-wrap gap-4 text-xs text-text-secondary\">\n <span>Total rows: {rows.length}</span>\n <span>Ready: {readyRows.length}</span>\n <span>With errors: {invalidRows.length}</span>\n {invalidRows.length > 0 ? (\n <Badge variant=\"destructive\">Invalid rows will be skipped</Badge>\n ) : null}\n </div>\n {invalidRows.length > 0 ? (\n <div className=\"rounded-md border border-status-error-border/20 bg-status-error-bg/10 px-3 py-2 text-sm text-status-error-text\">\n {invalidRows.length} row{invalidRows.length === 1 ? '' : 's'} need\n attention. Only valid rows can be invited.\n </div>\n ) : null}\n <div className=\"overflow-hidden rounded-xl border\">\n <ResponsiveTable<PreviewRow>\n columns={previewColumns}\n data={rows}\n rowKey={(row) => `${row.rowNumber}-${row.email}`}\n />\n </div>\n </>\n ) : (\n <div className=\"rounded-xl border border-dashed p-6 text-center text-sm text-text-secondary\">\n Upload a CSV to preview invite rows here before sending.\n </div>\n )}\n\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={handleBulkInvite}\n disabled={readyRows.length === 0 || isCreatingBulkInvites || isSubmittingBulk}\n className=\"gap-2\"\n >\n {isCreatingBulkInvites || isSubmittingBulk ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Send className=\"size-4\" />\n )}\n {tWithFallback(t, 'workspaceInvite.sendBulkButton', 'Send Bulk Invites')}\n </Button>\n </div>\n </TabsContent>\n </Tabs>\n </CardContent>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" id=\"invite-pending-section\" className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Mail className=\"size-5 text-primary\" />\n {tWithFallback(t, 'workspaceInvite.pendingTitle', 'Pending Invitations')}\n <Badge variant=\"secondary\" className=\"tabular-nums\">\n {pendingInvitations.length}\n </Badge>\n </CardTitle>\n </CardHeader>\n <CardContent>\n {pendingFeedback ? (\n <div\n className={`mb-4 flex items-center gap-2 rounded-md border p-3 text-sm ${\n pendingFeedback.tone === 'success'\n ? 'border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-400'\n : 'border-status-error-border/20 bg-status-error-bg/10 text-status-error-text'\n }`}\n >\n {pendingFeedback.tone === 'success' ? (\n <CheckCircle2 className=\"size-4 shrink-0\" />\n ) : (\n <AlertCircle className=\"size-4 shrink-0\" />\n )}\n <span>{pendingFeedback.message}</span>\n </div>\n ) : null}\n {isLoadingInvitations ? (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-20 w-full\" />\n <Skeleton className=\"h-20 w-full\" />\n </div>\n ) : invitationsError && pendingInvitations.length === 0 ? (\n <div\n role=\"alert\"\n className=\"space-y-3 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\"\n >\n <p>\n {tWithFallback(\n t,\n 'workspaceInvite.pendingLoadFailed',\n 'Failed to load pending invitations for this workspace.'\n )}\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-fit\"\n disabled={isFetchingInvitations}\n onClick={() => {\n void refetchInvitations();\n }}\n >\n {isFetchingInvitations\n ? tWithFallback(t, 'workspaceInvite.retrying', 'Retrying…')\n : tWithFallback(t, 'workspaceInvite.tryAgain', 'Try again')}\n </Button>\n </div>\n ) : pendingInvitations.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={tWithFallback(\n t,\n 'workspaceInvite.pendingEmptyTitle',\n 'No pending invitations'\n )}\n description={tWithFallback(\n t,\n 'workspaceInvite.pendingEmpty',\n 'Invitations you send for this workspace will show up here until they are accepted, so you can resend or cancel them.'\n )}\n />\n ) : (\n <div className=\"space-y-3\">\n {invitationsError ? (\n <div\n role=\"alert\"\n className=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\"\n >\n <span>\n {tWithFallback(\n t,\n 'workspaceInvite.pendingStaleWarning',\n 'Pending invitations may be outdated. Try again to refresh this list.'\n )}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-fit\"\n disabled={isFetchingInvitations}\n onClick={() => {\n void refetchInvitations();\n }}\n >\n {isFetchingInvitations\n ? tWithFallback(t, 'workspaceInvite.retrying', 'Retrying…')\n : tWithFallback(t, 'workspaceInvite.tryAgain', 'Try again')}\n </Button>\n </div>\n ) : null}\n {pendingInvitations.map((invitation) => {\n const roleNames = getRoleDisplayNames(invitation);\n const projectNames = getProjectDisplayNames(invitation);\n const isRunningAction =\n invitationActionId === invitation.id &&\n (isResendingInvitation || isCancellingInvitation);\n\n return (\n <div key={invitation.id} className=\"rounded-lg border p-4\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"font-semibold\">{invitation.email}</p>\n <Badge variant=\"outline\">{invitation.status}</Badge>\n </div>\n <div className=\"grid gap-3 text-sm text-text-secondary sm:grid-cols-2\">\n <div>\n <p className=\"font-medium text-text-primary\">Received</p>\n <p>{formatDate(invitation.createdAt) ?? '—'}</p>\n </div>\n <div>\n <p className=\"font-medium text-text-primary\">Expires</p>\n <p>{formatDate(invitation.expiresAt) ?? '—'}</p>\n </div>\n </div>\n <div>\n <p className=\"mb-2 text-sm font-medium text-text-primary\">Roles</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleNames.length > 0 ? (\n roleNames.map((roleName, index) => (\n <Badge\n key={`${invitation.id}-${invitation.roleIds[index] ?? roleName}`}\n variant=\"outline\"\n >\n {roleName}\n </Badge>\n ))\n ) : (\n <span className=\"text-sm text-text-secondary\">\n No roles selected\n </span>\n )}\n </div>\n </div>\n <div>\n <p className=\"mb-2 text-sm font-medium text-text-primary\">Projects</p>\n <div className=\"flex flex-wrap gap-2\">\n {projectNames.length > 0 ? (\n projectNames.map((projectName, index) => (\n <Badge\n key={`${invitation.id}-project-${projectName}-${index}`}\n variant=\"secondary\"\n >\n {projectName}\n </Badge>\n ))\n ) : (\n <span className=\"text-sm text-text-secondary\">\n No project access will be granted automatically\n </span>\n )}\n </div>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={isResendingInvitation || isCancellingInvitation}\n onClick={() => handleResendInvitation(invitation)}\n className=\"gap-2\"\n >\n {isRunningAction && isResendingInvitation ? (\n <RefreshCw className=\"size-4 animate-spin\" />\n ) : (\n <RefreshCw className=\"size-4\" />\n )}\n Resend\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={isResendingInvitation || isCancellingInvitation}\n onClick={() => setCancelTarget(invitation)}\n className=\"gap-2\"\n >\n {isRunningAction && isCancellingInvitation ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <XCircle className=\"size-4\" />\n )}\n Cancel\n </Button>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </CardContent>\n </GlassCard>\n </div>\n </>\n );\n}\n\nexport function WorkspaceInvitePage() {\n const { workspaceId: routeWorkspaceId } = useParams<{ workspaceId: string }>();\n const { workspaceId: contextWorkspaceId } = useWorkspacesContext();\n const permissionScopeId = routeWorkspaceId || contextWorkspaceId || null;\n\n return (\n <PermissionProvider scopeId={permissionScopeId} gateway=\"workspace\" debug={false}>\n <WorkspaceInvitePageContent />\n </PermissionProvider>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkspaceInvitePage.js","names":[],"sources":["../../src/pages/WorkspaceInvitePage.tsx"],"sourcesContent":["import { useMemo, useRef, useState } from 'react';\nimport { useNavigate, useParams } from 'react-router';\nimport {\n AlertCircle,\n ArrowLeft,\n CheckCircle2,\n Download,\n Loader2,\n Mail,\n RefreshCw,\n Send,\n Upload,\n UserPlus,\n XCircle,\n} from 'lucide-react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Badge,\n Button,\n GlassCard,\n CardContent,\n CardHeader,\n CardTitle,\n Checkbox,\n Input,\n Skeleton,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n ResponsiveTable,\n PagePurpose,\n NextSteps,\n IllustratedEmptyState,\n} from '@burdenoff/fe-libs/ui';\nimport type { ResponsiveTableColumn } from '@burdenoff/fe-libs/ui';\nimport { PermissionProvider } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../utils/i18n';\nimport { getProjectDisplayNames, getRoleDisplayNames } from '../utils/invitationDisplay';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspace } from '../hooks/useWorkspaces';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspaceRoles } from '../hooks/useWorkspaceRoles';\nimport { useProjects } from '../hooks/useProjects';\nimport { useWorkspaceInvitations } from '../hooks/useWorkspaceMembers';\nimport {\n useBulkCreateInvitations,\n useCancelInvitation,\n useCreateInvitation,\n useResendInvitation,\n} from '../hooks/useMemberMutations';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport type { Project, WorkspaceInvitation } from '../types';\nimport {\n downloadWorkspaceBulkInviteTemplate,\n parseWorkspaceBulkInviteCsv,\n type WorkspaceBulkInviteRow,\n} from '../utils/workspaceBulkInviteCsv';\n\ntype InviteMode = 'single' | 'bulk';\n\ntype RowStatus =\n | { kind: 'idle' }\n | { kind: 'sending' }\n | { kind: 'invited' }\n | { kind: 'failed'; message: string };\n\ninterface PreviewRow extends WorkspaceBulkInviteRow {\n status: RowStatus;\n}\n\ntype FeedbackState = {\n tone: 'success' | 'error';\n message: string;\n} | null;\n\nconst MAX_BULK_INVITE_FILE_SIZE = 1024 * 1024;\nconst MAX_BULK_INVITE_ROWS = 500;\n\nconst previewColumns: ResponsiveTableColumn<PreviewRow>[] = [\n {\n key: 'rowNumber',\n header: '#',\n priority: 'secondary',\n headerClassName: 'text-xs font-semibold uppercase tracking-wide text-text-secondary',\n cellClassName: 'text-xs text-text-secondary tabular-nums',\n cell: (row) => row.rowNumber,\n },\n {\n key: 'email',\n header: 'Email',\n priority: 'primary',\n headerClassName: 'text-xs font-semibold uppercase tracking-wide text-text-secondary',\n cellClassName: 'text-sm',\n cell: (row) => row.email || <span className=\"italic text-text-secondary\">(empty)</span>,\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'secondary',\n headerClassName: 'text-xs font-semibold uppercase tracking-wide text-text-secondary',\n cellClassName: 'text-xs',\n cell: (row) => {\n const hasParseErrors = row.errors.length > 0;\n if (hasParseErrors) {\n return (\n <div className=\"space-y-1 text-status-error-text\">\n {row.errors.map((errorText) => (\n <span\n key={`${row.rowNumber}-${errorText}`}\n className=\"inline-flex items-center gap-1 pr-3\"\n >\n <XCircle size={14} />\n {errorText}\n </span>\n ))}\n </div>\n );\n }\n if (row.status.kind === 'sending') {\n return (\n <span className=\"inline-flex items-center gap-1 text-text-secondary\">\n <Loader2 size={14} className=\"animate-spin\" />\n Sending...\n </span>\n );\n }\n if (row.status.kind === 'invited') {\n return (\n <span className=\"inline-flex items-center gap-1 text-green-700 dark:text-green-400\">\n <CheckCircle2 size={14} />\n Invited\n </span>\n );\n }\n if (row.status.kind === 'failed') {\n return (\n <span className=\"inline-flex items-center gap-1 text-status-error-text\">\n <XCircle size={14} />\n {row.status.message}\n </span>\n );\n }\n return <span className=\"text-text-secondary\">Ready</span>;\n },\n },\n];\n\nfunction isCsvFile(file: File): boolean {\n return file.type === 'text/csv' || file.name.toLowerCase().endsWith('.csv');\n}\n\nfunction formatDate(value?: string) {\n if (!value) return null;\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return null;\n\n return date.toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nfunction formatInviteError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('CONFLICT')) {\n return 'An invite already exists for this email address.';\n }\n if (message.includes('already a member')) {\n return 'This user is already a member of the workspace.';\n }\n return 'Something went wrong. Please try again or contact support.';\n}\n\nfunction formatSelectionSummary(\n items: string[],\n emptyLabel: string,\n formatter?: (value: string) => string\n) {\n if (items.length === 0) {\n return emptyLabel;\n }\n\n return items.map((item) => (formatter ? formatter(item) : item)).join(', ');\n}\n\nfunction buildInvitationPayload(\n workspaceId: string,\n email: string,\n roleIds: string[],\n projectIds: string[]\n) {\n return {\n email,\n workspaceId,\n roleIds,\n ...(projectIds.length > 0\n ? {\n context: {\n type: 'workspace.invitation',\n metadata: { projectIds },\n },\n }\n : {}),\n };\n}\n\nfunction WorkspaceSelectorSection({\n selectedRoleIds,\n onRoleToggle,\n selectedProjectIds,\n onProjectToggle,\n availableRoles,\n availableProjects,\n isLoadingRoles,\n isLoadingProjects,\n disabled,\n}: {\n selectedRoleIds: string[];\n onRoleToggle: (roleId: string, checked: boolean) => void;\n selectedProjectIds: string[];\n onProjectToggle: (projectId: string, checked: boolean) => void;\n availableRoles: Array<{\n id: string;\n name: string;\n description?: string | null;\n isSystem: boolean;\n }>;\n availableProjects: Project[];\n isLoadingRoles: boolean;\n isLoadingProjects: boolean;\n disabled: boolean;\n}) {\n return (\n <div className=\"space-y-6\">\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">Roles (Optional)</h3>\n <p className=\"text-sm text-text-secondary\">\n Choose roles. Hover any role to preview its full description.\n </p>\n </div>\n {selectedRoleIds.length > 0 ? (\n <Badge variant=\"secondary\">{selectedRoleIds.length} selected</Badge>\n ) : null}\n </div>\n <TooltipProvider>\n <div className=\"grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3\">\n {isLoadingRoles ? (\n <p className=\"text-sm text-text-secondary\">Loading roles…</p>\n ) : availableRoles.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No assignable roles were returned for this workspace.\n </p>\n ) : (\n availableRoles.map((role) => {\n const isSelected = selectedRoleIds.includes(role.id);\n return (\n <Tooltip key={role.id}>\n <TooltipTrigger asChild>\n <label\n className={`flex cursor-pointer items-center justify-between gap-3 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors focus-within:ring-2 focus-within:ring-[var(--color-focus-ring)] focus-within:ring-offset-2 ${\n isSelected\n ? 'border-primary bg-action-primary-bg/10 text-text-primary shadow-sm'\n : 'border-border-subtle bg-bg-surface text-text-primary hover:bg-accent'\n } ${disabled ? 'opacity-70' : ''}`}\n >\n <span className=\"min-w-0 flex-1 truncate\">{role.name}</span>\n <Checkbox\n checked={isSelected}\n disabled={disabled}\n onCheckedChange={(checked) => onRoleToggle(role.id, Boolean(checked))}\n aria-label={`Select ${role.name}`}\n />\n </label>\n </TooltipTrigger>\n <TooltipContent className=\"max-w-xs text-sm leading-6\">\n <p className=\"font-medium\">{role.name}</p>\n <p className=\"mt-1 text-text-secondary\">\n {role.description || 'No description available for this role.'}\n </p>\n {role.isSystem ? <p className=\"mt-2 text-xs\">System role</p> : null}\n </TooltipContent>\n </Tooltip>\n );\n })\n )}\n </div>\n </TooltipProvider>\n </div>\n\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div className=\"space-y-1\">\n <h3 className=\"text-base font-semibold\">Projects to add on acceptance (Optional)</h3>\n <p className=\"text-sm text-text-secondary\">\n The invited user will be added to these projects automatically after they accept.\n </p>\n </div>\n {selectedProjectIds.length > 0 ? (\n <Badge variant=\"secondary\">{selectedProjectIds.length} selected</Badge>\n ) : null}\n </div>\n <div className=\"grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3\">\n {isLoadingProjects ? (\n <p className=\"text-sm text-text-secondary\">Loading projects…</p>\n ) : availableProjects.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No active projects are available in this workspace yet.\n </p>\n ) : (\n availableProjects.map((project) => {\n const isSelected = selectedProjectIds.includes(project.id);\n return (\n <label\n key={project.id}\n className={`flex cursor-pointer items-start justify-between gap-3 rounded-lg border px-3 py-3 transition-colors focus-within:ring-2 focus-within:ring-[var(--color-focus-ring)] focus-within:ring-offset-2 ${\n isSelected\n ? 'border-primary bg-action-primary-bg/10 text-text-primary shadow-sm'\n : 'border-border-subtle bg-bg-surface text-text-primary hover:bg-accent'\n } ${disabled ? 'opacity-70' : ''}`}\n >\n <div className=\"min-w-0 flex-1 space-y-1\">\n <p className=\"truncate text-sm font-semibold leading-tight\">{project.name}</p>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <span className=\"truncate font-mono\">{project.key}</span>\n <Badge variant=\"outline\">{project.status}</Badge>\n </div>\n </div>\n <Checkbox\n checked={isSelected}\n disabled={disabled}\n onCheckedChange={(checked) => onProjectToggle(project.id, Boolean(checked))}\n aria-label={`Select ${project.name}`}\n />\n </label>\n );\n })\n )}\n </div>\n </div>\n </div>\n );\n}\n\nfunction WorkspaceInvitePageContent() {\n const { workspaceId: routeWorkspaceId } = useParams<{ workspaceId: string }>();\n const { t } = useI18n();\n const navigate = useNavigate();\n const { basePath = '/workspaces', workspaceId: contextWorkspaceId } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const workspaceId = routeWorkspaceId || contextWorkspaceId || '';\n\n useWorkspacesPageView('workspaces.invite', { workspaceId: workspaceId || undefined });\n\n const { canInviteMember, isLoading: permissionsLoading } = useWorkspacePermissions();\n const {\n data: workspace,\n isLoading: isLoadingWorkspace,\n error: workspaceError,\n } = useWorkspace(workspaceId);\n const { data: availableRoles = [], isLoading: isLoadingRoles } = useWorkspaceRoles(workspaceId);\n const { data: projectList, isLoading: isLoadingProjects } = useProjects(\n { archived: false },\n { limit: 100 },\n workspaceId\n );\n const {\n data: invitationsData,\n isLoading: isLoadingInvitations,\n isFetching: isFetchingInvitations,\n error: invitationsError,\n refetch: refetchInvitations,\n } = useWorkspaceInvitations(workspaceId, 'PENDING', { limit: 50 }, { retry: false });\n const availableProjects = useMemo(\n () => (projectList?.items ?? []).filter((project) => !project.archived),\n [projectList?.items]\n );\n\n const [mode, setMode] = useState<InviteMode>('single');\n const [email, setEmail] = useState('');\n const [selectedRoleIds, setSelectedRoleIds] = useState<string[]>([]);\n const [selectedProjectIds, setSelectedProjectIds] = useState<string[]>([]);\n const [singleError, setSingleError] = useState<string | null>(null);\n const [singleSuccess, setSingleSuccess] = useState<string | null>(null);\n const [bulkError, setBulkError] = useState<string | null>(null);\n const [bulkSuccess, setBulkSuccess] = useState<string | null>(null);\n const [pendingFeedback, setPendingFeedback] = useState<FeedbackState>(null);\n const [rows, setRows] = useState<PreviewRow[]>([]);\n const [fileName, setFileName] = useState<string | null>(null);\n const [dragActive, setDragActive] = useState(false);\n const [isSubmittingBulk, setIsSubmittingBulk] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const [cancelTarget, setCancelTarget] = useState<WorkspaceInvitation | null>(null);\n\n const { mutateAsync: createInvitation, isPending: isCreatingInvitation } =\n useCreateInvitation(workspaceId);\n const { mutateAsync: bulkCreateInvitations, isPending: isCreatingBulkInvites } =\n useBulkCreateInvitations();\n const { mutate: resendInvitation, isPending: isResendingInvitation } =\n useResendInvitation(workspaceId);\n const { mutate: cancelInvitation, isPending: isCancellingInvitation } =\n useCancelInvitation(workspaceId);\n const [invitationActionId, setInvitationActionId] = useState<string | null>(null);\n\n const pendingInvitations = invitationsData?.items ?? [];\n const isMutatingInvite = isCreatingInvitation || isCreatingBulkInvites || isSubmittingBulk;\n const selectedRoleNames = useMemo(\n () =>\n availableRoles.filter((role) => selectedRoleIds.includes(role.id)).map((role) => role.name),\n [availableRoles, selectedRoleIds]\n );\n const selectedProjectNames = useMemo(\n () =>\n availableProjects\n .filter((project) => selectedProjectIds.includes(project.id))\n .map((project) => project.name),\n [availableProjects, selectedProjectIds]\n );\n\n const readyRows = useMemo(\n () => rows.filter((row) => row.errors.length === 0 && row.status.kind !== 'invited'),\n [rows]\n );\n const invalidRows = useMemo(() => rows.filter((row) => row.errors.length > 0), [rows]);\n\n if (!workspaceId) {\n return (\n <div className=\"space-y-4 p-6\">\n <div className=\"rounded-xl border bg-bg-surface p-8 text-center\">\n <h1 className=\"text-2xl font-bold\">\n {tWithFallback(t, 'workspaceInvite.noWorkspaceTitle', 'Select a workspace first')}\n </h1>\n <p className=\"mt-2 text-text-secondary\">\n {tWithFallback(\n t,\n 'workspaceInvite.noWorkspaceDescription',\n 'Choose a current workspace from the header or open a workspace detail page before inviting members.'\n )}\n </p>\n <div className=\"mt-4\">\n <Button\n onClick={() => navigate(withCurrentSearch(`${basePath}/list`))}\n variant=\"outline\"\n >\n {tWithFallback(t, 'workspaceInvite.backToWorkspaces', 'Back to Workspaces')}\n </Button>\n </div>\n </div>\n </div>\n );\n }\n\n if (permissionsLoading || isLoadingWorkspace) {\n return (\n <div className=\"space-y-6 p-6\">\n <Skeleton className=\"h-10 w-48\" />\n <Skeleton className=\"h-40 w-full\" />\n <Skeleton className=\"h-80 w-full\" />\n </div>\n );\n }\n\n if (!canInviteMember) {\n return (\n <div className=\"flex min-h-[400px] items-center justify-center\">\n <div className=\"space-y-2 text-center\">\n <h2 className=\"text-lg font-semibold\">\n {tWithFallback(t, 'workspaceInvite.accessDenied', 'Access Denied')}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'workspaceInvite.accessDeniedDescription',\n \"You don't have permission to invite members into this workspace.\"\n )}\n </p>\n </div>\n </div>\n );\n }\n\n if (workspaceError || !workspace) {\n return (\n <div className=\"space-y-4 p-6\">\n <Button\n variant=\"ghost\"\n onClick={() => navigate(withCurrentSearch(`${basePath}/list`))}\n className=\"gap-2\"\n >\n <ArrowLeft size={18} />\n {tWithFallback(t, 'workspaceInvite.backToWorkspaces', 'Back to Workspaces')}\n </Button>\n <div className=\"rounded-xl border bg-bg-surface p-8 text-center text-status-error-text\">\n {tWithFallback(\n t,\n 'workspaceInvite.workspaceLoadError',\n 'Unable to load workspace invite details.'\n )}\n </div>\n </div>\n );\n }\n\n const handleRoleToggle = (roleId: string, checked: boolean) => {\n setSelectedRoleIds((prev) =>\n checked\n ? [...prev, roleId].filter((value, index, array) => array.indexOf(value) === index)\n : prev.filter((id) => id !== roleId)\n );\n };\n\n const handleProjectToggle = (projectId: string, checked: boolean) => {\n setSelectedProjectIds((prev) =>\n checked\n ? [...prev, projectId].filter((value, index, array) => array.indexOf(value) === index)\n : prev.filter((id) => id !== projectId)\n );\n };\n\n const handleSingleInvite = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n setSingleError(null);\n setSingleSuccess(null);\n setPendingFeedback(null);\n\n if (!email.trim()) {\n setSingleError('Email is required.');\n return;\n }\n\n try {\n await createInvitation(\n buildInvitationPayload(\n workspaceId,\n email.trim().toLowerCase(),\n selectedRoleIds,\n selectedProjectIds\n )\n );\n setEmail('');\n setSingleSuccess(`Invitation sent to ${email.trim().toLowerCase()}.`);\n } catch (error) {\n setSingleError(formatInviteError(error));\n }\n };\n\n const handleFile = (file: File) => {\n if (!isCsvFile(file)) {\n setBulkError('Please upload a .csv file.');\n return;\n }\n\n if (file.size > MAX_BULK_INVITE_FILE_SIZE) {\n setBulkError('CSV file is too large (max 1 MB).');\n return;\n }\n\n const reader = new FileReader();\n reader.onload = () => {\n const text = typeof reader.result === 'string' ? reader.result : '';\n const parsed = parseWorkspaceBulkInviteCsv(text);\n const limitedRows = parsed.slice(0, MAX_BULK_INVITE_ROWS);\n setRows(limitedRows.map((row) => ({ ...row, status: { kind: 'idle' } })));\n setFileName(file.name);\n setBulkError(\n parsed.length > MAX_BULK_INVITE_ROWS\n ? `Only the first ${MAX_BULK_INVITE_ROWS} rows were loaded. Split larger CSV files into smaller batches.`\n : null\n );\n setBulkSuccess(null);\n setPendingFeedback(null);\n };\n reader.onerror = () => {\n setBulkError('Failed to read CSV file.');\n };\n reader.readAsText(file);\n };\n\n const handleBulkInvite = async () => {\n if (readyRows.length === 0 || isSubmittingBulk) {\n return;\n }\n\n setBulkError(null);\n setBulkSuccess(null);\n setPendingFeedback(null);\n setIsSubmittingBulk(true);\n setRows((prev) =>\n prev.map((row) =>\n row.errors.length === 0 && row.status.kind !== 'invited'\n ? { ...row, status: { kind: 'sending' } }\n : row\n )\n );\n\n try {\n await bulkCreateInvitations(\n readyRows.map((row) =>\n buildInvitationPayload(workspaceId, row.email, selectedRoleIds, selectedProjectIds)\n )\n );\n\n setRows((prev) =>\n prev.map((row) =>\n row.status.kind === 'sending' ? { ...row, status: { kind: 'invited' } } : row\n )\n );\n setBulkSuccess(\n `Sent ${readyRows.length} workspace invitation${readyRows.length === 1 ? '' : 's'}.`\n );\n } catch (error) {\n const message = formatInviteError(error);\n setRows((prev) =>\n prev.map((row) =>\n row.status.kind === 'sending' ? { ...row, status: { kind: 'failed', message } } : row\n )\n );\n setBulkError(message);\n } finally {\n setIsSubmittingBulk(false);\n }\n };\n\n const handleResendInvitation = (invitation: WorkspaceInvitation) => {\n setPendingFeedback(null);\n setInvitationActionId(invitation.id);\n resendInvitation(invitation.id, {\n onSuccess: () => {\n setPendingFeedback({\n tone: 'success',\n message: `Invitation resent to ${invitation.email}.`,\n });\n },\n onError: (error) => {\n setPendingFeedback({ tone: 'error', message: formatInviteError(error) });\n },\n onSettled: () => setInvitationActionId(null),\n });\n };\n\n const handleConfirmCancel = () => {\n if (!cancelTarget) {\n return;\n }\n\n setPendingFeedback(null);\n setInvitationActionId(cancelTarget.id);\n cancelInvitation(cancelTarget.id, {\n onSuccess: () => {\n setPendingFeedback({\n tone: 'success',\n message: `Invitation cancelled for ${cancelTarget.email}.`,\n });\n setCancelTarget(null);\n },\n onError: (error) => {\n setPendingFeedback({ tone: 'error', message: formatInviteError(error) });\n },\n onSettled: () => setInvitationActionId(null),\n });\n };\n\n return (\n <>\n <AlertDialog\n open={Boolean(cancelTarget)}\n onOpenChange={(open) => !open && setCancelTarget(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Cancel pending invitation?</AlertDialogTitle>\n <AlertDialogDescription>\n {cancelTarget\n ? `This will revoke the pending invite for ${cancelTarget.email}. They will need a new invitation to join later.`\n : 'This will revoke the pending invitation.'}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={isCancellingInvitation}>Keep invitation</AlertDialogCancel>\n <AlertDialogAction onClick={handleConfirmCancel} disabled={isCancellingInvitation}>\n {isCancellingInvitation ? <Loader2 className=\"size-4 animate-spin\" /> : null}\n Cancel invitation\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n\n <div className=\"space-y-6 p-3 sm:p-6\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <Button\n variant=\"ghost\"\n onClick={() => navigate(withCurrentSearch(`${basePath}/${workspaceId}`))}\n className=\"-ml-2 mb-2 gap-2\"\n >\n <ArrowLeft size={18} />\n {tWithFallback(t, 'workspaceInvite.backToWorkspace', 'Back to Workspace')}\n </Button>\n <div className=\"flex items-center gap-2\">\n <UserPlus className=\"text-primary\" size={24} />\n <h1 className=\"text-2xl font-bold sm:text-3xl\">\n {tWithFallback(t, 'workspaceInvite.title', 'Invite Members')}\n </h1>\n </div>\n <p className=\"mt-2 text-sm text-text-secondary sm:text-base\">\n {workspace.name} —{' '}\n {tWithFallback(\n t,\n 'workspaceInvite.subtitle',\n 'Invite people into this workspace and optionally grant project access.'\n )}\n </p>\n </div>\n </div>\n\n <PagePurpose>\n {tWithFallback(\n t,\n 'workspaceInvite.purpose',\n 'Use this page to bring new people into the workspace. First choose the roles and projects they should get the moment they accept, then send a single invitation by email or upload a CSV to invite many people at once. Invitations you have already sent appear at the bottom, where you can resend or cancel them.'\n )}\n </PagePurpose>\n\n <NextSteps\n storageKey=\"workspaces-workspace-invite\"\n steps={[\n {\n id: 'pick-access',\n label: tWithFallback(\n t,\n 'workspaceInvite.stepAccess',\n 'Pick the roles and projects to grant'\n ),\n description: tWithFallback(\n t,\n 'workspaceInvite.stepAccessDesc',\n 'These are applied automatically when each person accepts.'\n ),\n icon: <UserPlus size={16} />,\n onClick: () =>\n document\n .getElementById('invite-access-section')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n {\n id: 'send-invite',\n label: tWithFallback(t, 'workspaceInvite.stepSend', 'Send the invitation'),\n description: tWithFallback(\n t,\n 'workspaceInvite.stepSendDesc',\n 'Enter one email, or switch to the Bulk CSV tab to invite many at once.'\n ),\n icon: <Send size={16} />,\n onClick: () =>\n document\n .getElementById('invite-send-section')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n {\n id: 'review-pending',\n label: tWithFallback(\n t,\n 'workspaceInvite.stepPending',\n 'Track your pending invitations'\n ),\n description: tWithFallback(\n t,\n 'workspaceInvite.stepPendingDesc',\n 'Resend or cancel invitations that have not been accepted yet.'\n ),\n icon: <Mail size={16} />,\n onClick: () =>\n document\n .getElementById('invite-pending-section')\n ?.scrollIntoView({ behavior: 'smooth', block: 'start' }),\n },\n ]}\n />\n\n <GlassCard treatment=\"plain\" id=\"invite-access-section\" className=\"border-border-seam\">\n <CardHeader>\n <CardTitle>\n {tWithFallback(t, 'workspaceInvite.setupTitle', 'Access to grant on acceptance')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <WorkspaceSelectorSection\n selectedRoleIds={selectedRoleIds}\n onRoleToggle={handleRoleToggle}\n selectedProjectIds={selectedProjectIds}\n onProjectToggle={handleProjectToggle}\n availableRoles={availableRoles}\n availableProjects={availableProjects}\n isLoadingRoles={isLoadingRoles}\n isLoadingProjects={isLoadingProjects}\n disabled={isMutatingInvite}\n />\n </CardContent>\n </GlassCard>\n\n {/* The one emphasized zone: live selection summary */}\n <GlassCard treatment=\"metric\" glow className=\"border-border-seam\">\n <CardHeader>\n <CardTitle>Selection summary</CardTitle>\n </CardHeader>\n <CardContent className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2 rounded-lg border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"text-sm font-medium\">Roles</p>\n <Badge variant=\"secondary\" className=\"tabular-nums\">\n {selectedRoleNames.length}\n </Badge>\n </div>\n <p className=\"text-sm text-text-muted\">\n {formatSelectionSummary(selectedRoleNames, 'No roles selected')}\n </p>\n </div>\n <div className=\"space-y-2 rounded-lg border border-border-seam bg-bg-surface p-4\">\n <div className=\"flex items-center justify-between gap-2\">\n <p className=\"text-sm font-medium\">Projects</p>\n <Badge variant=\"secondary\" className=\"tabular-nums\">\n {selectedProjectNames.length}\n </Badge>\n </div>\n <p className=\"text-sm text-text-muted\">\n {formatSelectionSummary(\n selectedProjectNames,\n 'No project access will be granted automatically'\n )}\n </p>\n </div>\n </CardContent>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" id=\"invite-send-section\" className=\"border-border-seam\">\n <CardHeader>\n <CardTitle>\n {tWithFallback(t, 'workspaceInvite.sendTitle', 'Send invitations')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Tabs\n value={mode}\n onValueChange={(value) => setMode(value as InviteMode)}\n className=\"space-y-5\"\n >\n <TabsList>\n <TabsTrigger value=\"single\">\n {tWithFallback(t, 'workspaceInvite.modeSingle', 'Single')}\n </TabsTrigger>\n <TabsTrigger value=\"bulk\">\n {tWithFallback(t, 'workspaceInvite.modeBulk', 'Bulk CSV')}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"single\" className=\"space-y-4\">\n {singleError ? (\n <div className=\"flex items-center gap-2 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 shrink-0\" />\n <span>{singleError}</span>\n </div>\n ) : null}\n {singleSuccess ? (\n <div className=\"flex items-center gap-2 rounded-md border border-green-500/20 bg-green-500/10 p-3 text-sm text-green-700 dark:text-green-400\">\n <CheckCircle2 className=\"size-4 shrink-0\" />\n <span>{singleSuccess}</span>\n </div>\n ) : null}\n\n <form onSubmit={handleSingleInvite} className=\"space-y-4\">\n <div className=\"space-y-2\">\n <label htmlFor=\"workspace-invite-email\" className=\"text-sm font-medium\">\n {tWithFallback(t, 'workspaceInvite.emailLabel', 'Email Address')}\n </label>\n <Input\n id=\"workspace-invite-email\"\n type=\"email\"\n placeholder={tWithFallback(\n t,\n 'workspaceInvite.emailPlaceholder',\n 'member@example.com'\n )}\n value={email}\n onChange={(event) => setEmail(event.target.value)}\n disabled={isCreatingInvitation}\n />\n </div>\n\n <div className=\"flex justify-end\">\n <Button type=\"submit\" disabled={isCreatingInvitation} className=\"gap-2\">\n {isCreatingInvitation ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Send className=\"size-4\" />\n )}\n {tWithFallback(t, 'workspaceInvite.sendSingleButton', 'Send Invitation')}\n </Button>\n </div>\n </form>\n </TabsContent>\n\n <TabsContent value=\"bulk\" className=\"space-y-5\">\n {bulkError ? (\n <div className=\"flex items-center gap-2 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\">\n <AlertCircle className=\"size-4 shrink-0\" />\n <span>{bulkError}</span>\n </div>\n ) : null}\n {bulkSuccess ? (\n <div className=\"flex items-center gap-2 rounded-md border border-green-500/20 bg-green-500/10 p-3 text-sm text-green-700 dark:text-green-400\">\n <CheckCircle2 className=\"size-4 shrink-0\" />\n <span>{bulkSuccess}</span>\n </div>\n ) : null}\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"space-y-1\">\n <p className=\"text-sm text-text-secondary\">\n {tWithFallback(\n t,\n 'workspaceInvite.bulkDescription',\n 'Upload a CSV with one email column. All valid rows will receive the currently selected roles and projects.'\n )}\n </p>\n <div className=\"rounded-md border bg-bg-sunken/30 px-3 py-2 text-xs text-text-secondary\">\n <p className=\"font-medium text-text-primary\">Sample format</p>\n <pre className=\"mt-1 whitespace-pre-wrap\">\n email{`\\n`}alice@example.com{`\\n`}bob@example.com\n </pre>\n </div>\n {(projectList?.total ?? 0) > availableProjects.length ? (\n <p className=\"text-xs text-text-secondary\">\n Showing the first {availableProjects.length} active projects. Narrow the\n workspace project list if you need to invite into more.\n </p>\n ) : null}\n </div>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={downloadWorkspaceBulkInviteTemplate}\n className=\"gap-2\"\n >\n <Download size={16} />\n {tWithFallback(t, 'workspaceInvite.downloadTemplate', 'Download template')}\n </Button>\n </div>\n\n <div\n onDragOver={(event) => {\n event.preventDefault();\n setDragActive(true);\n }}\n onDragLeave={() => setDragActive(false)}\n onDrop={(event) => {\n event.preventDefault();\n setDragActive(false);\n const file = event.dataTransfer.files?.[0];\n if (file) {\n handleFile(file);\n } else {\n setBulkError('Please upload a .csv file.');\n }\n }}\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-primary bg-action-primary-bg/5'\n : 'border-border-subtle bg-bg-surface'\n }`}\n >\n <Upload size={28} className=\"text-text-secondary\" />\n <div className=\"text-center\">\n <p className=\"text-sm font-medium\">\n {fileName || 'Drop a CSV file here or click to browse'}\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n CSV files only. Required column: email.\n </p>\n </div>\n <input\n ref={fileInputRef}\n type=\"file\"\n accept=\".csv,text/csv\"\n onChange={(event) => {\n const file = event.target.files?.[0];\n if (file) {\n handleFile(file);\n }\n event.target.value = '';\n }}\n className=\"hidden\"\n aria-label={tWithFallback(\n t,\n 'workspaceInvite.chooseCsvFileAriaLabel',\n 'Choose CSV file'\n )}\n />\n <Button type=\"button\" onClick={() => fileInputRef.current?.click()}>\n {tWithFallback(t, 'workspaceInvite.chooseFile', 'Choose file')}\n </Button>\n </div>\n\n {rows.length > 0 ? (\n <>\n <div className=\"flex flex-wrap gap-4 text-xs text-text-secondary\">\n <span>Total rows: {rows.length}</span>\n <span>Ready: {readyRows.length}</span>\n <span>With errors: {invalidRows.length}</span>\n {invalidRows.length > 0 ? (\n <Badge variant=\"destructive\">Invalid rows will be skipped</Badge>\n ) : null}\n </div>\n {invalidRows.length > 0 ? (\n <div className=\"rounded-md border border-status-error-border/20 bg-status-error-bg/10 px-3 py-2 text-sm text-status-error-text\">\n {invalidRows.length} row{invalidRows.length === 1 ? '' : 's'} need\n attention. Only valid rows can be invited.\n </div>\n ) : null}\n <div className=\"overflow-hidden rounded-xl border\">\n <ResponsiveTable<PreviewRow>\n columns={previewColumns}\n data={rows}\n rowKey={(row) => `${row.rowNumber}-${row.email}`}\n />\n </div>\n </>\n ) : (\n <div className=\"rounded-xl border border-dashed p-6 text-center text-sm text-text-secondary\">\n Upload a CSV to preview invite rows here before sending.\n </div>\n )}\n\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n onClick={handleBulkInvite}\n disabled={readyRows.length === 0 || isCreatingBulkInvites || isSubmittingBulk}\n className=\"gap-2\"\n >\n {isCreatingBulkInvites || isSubmittingBulk ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <Send className=\"size-4\" />\n )}\n {tWithFallback(t, 'workspaceInvite.sendBulkButton', 'Send Bulk Invites')}\n </Button>\n </div>\n </TabsContent>\n </Tabs>\n </CardContent>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" id=\"invite-pending-section\" className=\"border-border-seam\">\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Mail className=\"size-5 text-primary\" />\n {tWithFallback(t, 'workspaceInvite.pendingTitle', 'Pending Invitations')}\n <Badge variant=\"secondary\" className=\"tabular-nums\">\n {pendingInvitations.length}\n </Badge>\n </CardTitle>\n </CardHeader>\n <CardContent>\n {pendingFeedback ? (\n <div\n className={`mb-4 flex items-center gap-2 rounded-md border p-3 text-sm ${\n pendingFeedback.tone === 'success'\n ? 'border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-400'\n : 'border-status-error-border/20 bg-status-error-bg/10 text-status-error-text'\n }`}\n >\n {pendingFeedback.tone === 'success' ? (\n <CheckCircle2 className=\"size-4 shrink-0\" />\n ) : (\n <AlertCircle className=\"size-4 shrink-0\" />\n )}\n <span>{pendingFeedback.message}</span>\n </div>\n ) : null}\n {isLoadingInvitations ? (\n <div className=\"space-y-3\">\n <Skeleton className=\"h-20 w-full\" />\n <Skeleton className=\"h-20 w-full\" />\n </div>\n ) : invitationsError && pendingInvitations.length === 0 ? (\n <div\n role=\"alert\"\n className=\"space-y-3 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\"\n >\n <p>\n {tWithFallback(\n t,\n 'workspaceInvite.pendingLoadFailed',\n 'Failed to load pending invitations for this workspace.'\n )}\n </p>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-fit\"\n disabled={isFetchingInvitations}\n onClick={() => {\n void refetchInvitations();\n }}\n >\n {isFetchingInvitations\n ? tWithFallback(t, 'workspaceInvite.retrying', 'Retrying…')\n : tWithFallback(t, 'workspaceInvite.tryAgain', 'Try again')}\n </Button>\n </div>\n ) : pendingInvitations.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={tWithFallback(\n t,\n 'workspaceInvite.pendingEmptyTitle',\n 'No pending invitations'\n )}\n description={tWithFallback(\n t,\n 'workspaceInvite.pendingEmpty',\n 'Invitations you send for this workspace will show up here until they are accepted, so you can resend or cancel them.'\n )}\n />\n ) : (\n <div className=\"space-y-3\">\n {invitationsError ? (\n <div\n role=\"alert\"\n className=\"flex flex-wrap items-center justify-between gap-3 rounded-md border border-status-error-border/20 bg-status-error-bg/10 p-3 text-sm text-status-error-text\"\n >\n <span>\n {tWithFallback(\n t,\n 'workspaceInvite.pendingStaleWarning',\n 'Pending invitations may be outdated. Try again to refresh this list.'\n )}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-fit\"\n disabled={isFetchingInvitations}\n onClick={() => {\n void refetchInvitations();\n }}\n >\n {isFetchingInvitations\n ? tWithFallback(t, 'workspaceInvite.retrying', 'Retrying…')\n : tWithFallback(t, 'workspaceInvite.tryAgain', 'Try again')}\n </Button>\n </div>\n ) : null}\n {pendingInvitations.map((invitation) => {\n const roleNames = getRoleDisplayNames(invitation);\n const projectNames = getProjectDisplayNames(invitation);\n const isRunningAction =\n invitationActionId === invitation.id &&\n (isResendingInvitation || isCancellingInvitation);\n\n return (\n <div key={invitation.id} className=\"rounded-lg border p-4\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"font-semibold\">{invitation.email}</p>\n <Badge variant=\"outline\">{invitation.status}</Badge>\n </div>\n <div className=\"grid gap-3 text-sm text-text-secondary sm:grid-cols-2\">\n <div>\n <p className=\"font-medium text-text-primary\">Received</p>\n <p>{formatDate(invitation.createdAt) ?? '—'}</p>\n </div>\n <div>\n <p className=\"font-medium text-text-primary\">Expires</p>\n <p>{formatDate(invitation.expiresAt) ?? '—'}</p>\n </div>\n </div>\n <div>\n <p className=\"mb-2 text-sm font-medium text-text-primary\">Roles</p>\n <div className=\"flex flex-wrap gap-2\">\n {roleNames.length > 0 ? (\n roleNames.map((roleName, index) => (\n <Badge\n key={`${invitation.id}-${invitation.roleIds[index] ?? roleName}`}\n variant=\"outline\"\n >\n {roleName}\n </Badge>\n ))\n ) : (\n <span className=\"text-sm text-text-secondary\">\n No roles selected\n </span>\n )}\n </div>\n </div>\n <div>\n <p className=\"mb-2 text-sm font-medium text-text-primary\">Projects</p>\n <div className=\"flex flex-wrap gap-2\">\n {projectNames.length > 0 ? (\n projectNames.map((projectName, index) => (\n <Badge\n key={`${invitation.id}-project-${projectName}-${index}`}\n variant=\"secondary\"\n >\n {projectName}\n </Badge>\n ))\n ) : (\n <span className=\"text-sm text-text-secondary\">\n No project access will be granted automatically\n </span>\n )}\n </div>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={isResendingInvitation || isCancellingInvitation}\n onClick={() => handleResendInvitation(invitation)}\n className=\"gap-2\"\n >\n {isRunningAction && isResendingInvitation ? (\n <RefreshCw className=\"size-4 animate-spin\" />\n ) : (\n <RefreshCw className=\"size-4\" />\n )}\n Resend\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={isResendingInvitation || isCancellingInvitation}\n onClick={() => setCancelTarget(invitation)}\n className=\"gap-2\"\n >\n {isRunningAction && isCancellingInvitation ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : (\n <XCircle className=\"size-4\" />\n )}\n Cancel\n </Button>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </CardContent>\n </GlassCard>\n </div>\n </>\n );\n}\n\nexport function WorkspaceInvitePage() {\n const { workspaceId: routeWorkspaceId } = useParams<{ workspaceId: string }>();\n const { workspaceId: contextWorkspaceId } = useWorkspacesContext();\n const permissionScopeId = routeWorkspaceId || contextWorkspaceId || null;\n\n return (\n <PermissionProvider scopeId={permissionScopeId} gateway=\"workspace\" debug={false}>\n <WorkspaceInvitePageContent />\n </PermissionProvider>\n );\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burdenoff/microfe-workspaces",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.829.1",
|
|
4
4
|
"description": "Workspaces microfrontend for Burdenoff products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -23,12 +23,12 @@
|
|
|
23
23
|
"types": "./dist/index.d.ts",
|
|
24
24
|
"scripts": {
|
|
25
25
|
"dev": "vite",
|
|
26
|
-
"build": "
|
|
26
|
+
"build": "bash scripts/build-limits.sh bash scripts/typecheck.sh && bash scripts/build-limits.sh --gate vite build",
|
|
27
27
|
"lint": "eslint .",
|
|
28
28
|
"lint:sanity": "if [ \"${CI:-}\" = \"true\" ]; then bun run lint; else bun run lint -- --cache --cache-location .eslintcache; fi",
|
|
29
29
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
30
30
|
"format:check": "prettier --check \"src/**/*.{ts,tsx}\"",
|
|
31
|
-
"type:check": "bash scripts/build-limits.sh
|
|
31
|
+
"type:check": "bash scripts/build-limits.sh bash scripts/typecheck.sh",
|
|
32
32
|
"test": "bash scripts/build-limits.sh vitest run",
|
|
33
33
|
"test:watch": "vitest",
|
|
34
34
|
"test:coverage": "bash scripts/build-limits.sh vitest run --coverage",
|