@burdenoff/microfe-workspaces 2026.520.1 → 2026.528.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/WorkspacesRoot.js +4 -1
- package/dist/WorkspacesRoot.js.map +1 -1
- package/dist/WorkspacesRoutes.js +4 -4
- package/dist/WorkspacesRoutes.js.map +1 -1
- package/dist/components/CreateProjectDialog.js +1 -1
- package/dist/components/CreateProjectDialog.js.map +1 -1
- package/dist/components/CreateWorkspaceDialog.js +2 -2
- package/dist/components/CreateWorkspaceDialog.js.map +1 -1
- package/dist/components/InviteMemberDialog.js +2 -2
- package/dist/components/InviteMemberDialog.js.map +1 -1
- package/dist/components/MemberListItem.js +71 -86
- package/dist/components/MemberListItem.js.map +1 -1
- package/dist/components/ProjectCard.js +12 -10
- package/dist/components/ProjectCard.js.map +1 -1
- package/dist/components/WorkspaceCard.js.map +1 -1
- package/dist/components/WorkspacesErrorBoundary.js +12 -4
- package/dist/components/WorkspacesErrorBoundary.js.map +1 -1
- package/dist/components/tags/ProjectTagsWidget.js +248 -0
- package/dist/components/tags/ProjectTagsWidget.js.map +1 -0
- package/dist/hooks/useOrgsWithPermission.js +1 -1
- package/dist/hooks/useOrgsWithPermission.js.map +1 -1
- package/dist/hooks/useWorkspacesEventEmitter.js.map +1 -1
- package/dist/hooks/useWorkspacesPageView.js +6 -4
- package/dist/hooks/useWorkspacesPageView.js.map +1 -1
- package/dist/pages/CreateWorkspacePage.js +239 -224
- package/dist/pages/CreateWorkspacePage.js.map +1 -1
- package/dist/pages/InviteAcceptancePage.js +28 -28
- package/dist/pages/InviteAcceptancePage.js.map +1 -1
- package/dist/pages/MembersListPage.js +380 -352
- package/dist/pages/MembersListPage.js.map +1 -1
- package/dist/pages/ProjectDetailPage.js +96 -97
- package/dist/pages/ProjectDetailPage.js.map +1 -1
- package/dist/pages/ProjectsListPage.js +307 -304
- package/dist/pages/ProjectsListPage.js.map +1 -1
- package/dist/pages/WorkspaceSettingsPage.js +94 -91
- package/dist/pages/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/WorkspacesListPage.js +1 -1
- package/dist/pages/WorkspacesListPage.js.map +1 -1
- package/dist/pages/dashboard/DashboardCustomizer.js +68 -68
- package/dist/pages/dashboard/DashboardCustomizer.js.map +1 -1
- package/dist/pages/dashboard/DashboardGrid.js +52 -41
- package/dist/pages/dashboard/DashboardGrid.js.map +1 -1
- package/dist/pages/dashboard/DashboardPage.js +74 -65
- package/dist/pages/dashboard/DashboardPage.js.map +1 -1
- package/dist/pages/dashboard/WidgetWrapper.js +3 -3
- package/dist/pages/dashboard/WidgetWrapper.js.map +1 -1
- package/dist/pages/dashboard/config.js +126 -30
- package/dist/pages/dashboard/config.js.map +1 -1
- package/dist/pages/dashboard/hooks/useDashboardData.js +39 -2
- package/dist/pages/dashboard/hooks/useDashboardData.js.map +1 -1
- package/dist/pages/dashboard/widgets/AgentsWidget.js +109 -0
- package/dist/pages/dashboard/widgets/AgentsWidget.js.map +1 -0
- package/dist/pages/dashboard/widgets/GroupsWidget.js +6 -5
- package/dist/pages/dashboard/widgets/GroupsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/MembersWidget.js +6 -6
- package/dist/pages/dashboard/widgets/MembersWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/ProjectsWidget.js +8 -7
- package/dist/pages/dashboard/widgets/ProjectsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/QuickActionsWidget.js +72 -18
- package/dist/pages/dashboard/widgets/QuickActionsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js +3 -3
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/SessionsWidget.js +78 -0
- package/dist/pages/dashboard/widgets/SessionsWidget.js.map +1 -0
- package/dist/pages/dashboard/widgets/StorageWidget.js +35 -20
- package/dist/pages/dashboard/widgets/StorageWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/TagCloudWidget.js +2 -2
- package/dist/pages/dashboard/widgets/TagCloudWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/TipsWidget.js +81 -22
- package/dist/pages/dashboard/widgets/TipsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.js +6 -5
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/VibesWidget.js +64 -0
- package/dist/pages/dashboard/widgets/VibesWidget.js.map +1 -0
- package/dist/pages/dashboard/widgets/WelcomeWidget.js +5 -5
- package/dist/pages/dashboard/widgets/WelcomeWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.js +3 -3
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WorkspacesWidget.js +6 -5
- package/dist/pages/dashboard/widgets/WorkspacesWidget.js.map +1 -1
- package/dist/providers/WorkspacesProvider.js +1 -1
- package/dist/providers/WorkspacesProvider.js.map +1 -1
- package/dist/store/workspacesStore.js.map +1 -1
- package/dist/utils/formatters.js +21 -16
- package/dist/utils/formatters.js.map +1 -1
- package/dist/utils/telemetryError.js +1 -1
- package/dist/utils/telemetryError.js.map +1 -1
- package/package.json +7 -7
- package/dist/WorkspacesRoot.d.ts +0 -35
- package/dist/WorkspacesRoot.d.ts.map +0 -1
- package/dist/WorkspacesRoutes.d.ts +0 -10
- package/dist/WorkspacesRoutes.d.ts.map +0 -1
- package/dist/components/CreateProjectDialog.d.ts +0 -6
- package/dist/components/CreateProjectDialog.d.ts.map +0 -1
- package/dist/components/CreateWorkspaceDialog.d.ts +0 -2
- package/dist/components/CreateWorkspaceDialog.d.ts.map +0 -1
- package/dist/components/InviteMemberDialog.d.ts +0 -7
- package/dist/components/InviteMemberDialog.d.ts.map +0 -1
- package/dist/components/MemberListItem.d.ts +0 -11
- package/dist/components/MemberListItem.d.ts.map +0 -1
- package/dist/components/ProjectCard.d.ts +0 -13
- package/dist/components/ProjectCard.d.ts.map +0 -1
- package/dist/components/WorkspaceCard.d.ts +0 -8
- package/dist/components/WorkspaceCard.d.ts.map +0 -1
- package/dist/components/WorkspacesErrorBoundary.d.ts +0 -27
- package/dist/components/WorkspacesErrorBoundary.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -8
- package/dist/components/index.d.ts.map +0 -1
- package/dist/constants/permissions.d.ts +0 -33
- package/dist/constants/permissions.d.ts.map +0 -1
- package/dist/generated/global-operations.d.ts +0 -319
- package/dist/generated/global-operations.d.ts.map +0 -1
- package/dist/generated/global-types.d.ts +0 -23527
- package/dist/generated/global-types.d.ts.map +0 -1
- package/dist/generated/wspace-operations.d.ts +0 -2300
- package/dist/generated/wspace-operations.d.ts.map +0 -1
- package/dist/generated/wspace-types.d.ts +0 -57851
- package/dist/generated/wspace-types.d.ts.map +0 -1
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/useMemberMutations.d.ts +0 -61
- package/dist/hooks/useMemberMutations.d.ts.map +0 -1
- package/dist/hooks/useOrgsWithPermission.d.ts +0 -7
- package/dist/hooks/useOrgsWithPermission.d.ts.map +0 -1
- package/dist/hooks/useProjectMutations.d.ts +0 -26
- package/dist/hooks/useProjectMutations.d.ts.map +0 -1
- package/dist/hooks/useProjects.d.ts +0 -16
- package/dist/hooks/useProjects.d.ts.map +0 -1
- package/dist/hooks/useScopedNavigation.d.ts +0 -4
- package/dist/hooks/useScopedNavigation.d.ts.map +0 -1
- package/dist/hooks/useTenants.d.ts +0 -13
- package/dist/hooks/useTenants.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceActivity.d.ts +0 -12
- package/dist/hooks/useWorkspaceActivity.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceMembers.d.ts +0 -24
- package/dist/hooks/useWorkspaceMembers.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceMutations.d.ts +0 -36
- package/dist/hooks/useWorkspaceMutations.d.ts.map +0 -1
- package/dist/hooks/useWorkspacePermissions.d.ts +0 -29
- package/dist/hooks/useWorkspacePermissions.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceRoles.d.ts +0 -8
- package/dist/hooks/useWorkspaceRoles.d.ts.map +0 -1
- package/dist/hooks/useWorkspaces.d.ts +0 -21
- package/dist/hooks/useWorkspaces.d.ts.map +0 -1
- package/dist/hooks/useWorkspacesEventEmitter.d.ts +0 -18
- package/dist/hooks/useWorkspacesEventEmitter.d.ts.map +0 -1
- package/dist/hooks/useWorkspacesPageView.d.ts +0 -27
- package/dist/hooks/useWorkspacesPageView.d.ts.map +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.d.ts.map +0 -1
- package/dist/pages/ActivityListPage.d.ts +0 -2
- package/dist/pages/ActivityListPage.d.ts.map +0 -1
- package/dist/pages/CreateWorkspacePage.d.ts +0 -2
- package/dist/pages/CreateWorkspacePage.d.ts.map +0 -1
- package/dist/pages/InviteAcceptancePage.d.ts +0 -6
- package/dist/pages/InviteAcceptancePage.d.ts.map +0 -1
- package/dist/pages/MembersListPage.d.ts +0 -2
- package/dist/pages/MembersListPage.d.ts.map +0 -1
- package/dist/pages/ProjectDetailPage.d.ts +0 -2
- package/dist/pages/ProjectDetailPage.d.ts.map +0 -1
- package/dist/pages/ProjectsListPage.d.ts +0 -2
- package/dist/pages/ProjectsListPage.d.ts.map +0 -1
- package/dist/pages/WorkspaceDetailPage.d.ts +0 -2
- package/dist/pages/WorkspaceDetailPage.d.ts.map +0 -1
- package/dist/pages/WorkspaceSettingsPage.d.ts +0 -2
- package/dist/pages/WorkspaceSettingsPage.d.ts.map +0 -1
- package/dist/pages/WorkspacesListPage.d.ts +0 -2
- package/dist/pages/WorkspacesListPage.d.ts.map +0 -1
- package/dist/pages/dashboard/DashboardCustomizer.d.ts +0 -12
- package/dist/pages/dashboard/DashboardCustomizer.d.ts.map +0 -1
- package/dist/pages/dashboard/DashboardGrid.d.ts +0 -8
- package/dist/pages/dashboard/DashboardGrid.d.ts.map +0 -1
- package/dist/pages/dashboard/DashboardPage.d.ts +0 -14
- package/dist/pages/dashboard/DashboardPage.d.ts.map +0 -1
- package/dist/pages/dashboard/WidgetWrapper.d.ts +0 -17
- package/dist/pages/dashboard/WidgetWrapper.d.ts.map +0 -1
- package/dist/pages/dashboard/config.d.ts +0 -9
- package/dist/pages/dashboard/config.d.ts.map +0 -1
- package/dist/pages/dashboard/hooks/useDashboardData.d.ts +0 -9
- package/dist/pages/dashboard/hooks/useDashboardData.d.ts.map +0 -1
- package/dist/pages/dashboard/hooks/useDashboardPreferences.d.ts +0 -4
- package/dist/pages/dashboard/hooks/useDashboardPreferences.d.ts.map +0 -1
- package/dist/pages/dashboard/types.d.ts +0 -145
- package/dist/pages/dashboard/types.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/GroupsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/GroupsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/MembersWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/MembersWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/ProjectsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/ProjectsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/QuickActionsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/QuickActionsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/RecentActivityWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/RecentActivityWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/StorageWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/StorageWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/SystemHealthWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/SystemHealthWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/SystemHealthWidget.js +0 -59
- package/dist/pages/dashboard/widgets/SystemHealthWidget.js.map +0 -1
- package/dist/pages/dashboard/widgets/TagCloudWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/TagCloudWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/TipsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/TipsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/WelcomeWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/WelcomeWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/WorkspacesWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/WorkspacesWidget.d.ts.map +0 -1
- package/dist/providers/WorkspacesProvider.d.ts +0 -13
- package/dist/providers/WorkspacesProvider.d.ts.map +0 -1
- package/dist/store/workspacesStore.d.ts +0 -82
- package/dist/store/workspacesStore.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -280
- package/dist/types/index.d.ts.map +0 -1
- package/dist/utils/formatters.d.ts +0 -60
- package/dist/utils/formatters.d.ts.map +0 -1
- package/dist/utils/i18n.d.ts +0 -13
- package/dist/utils/i18n.d.ts.map +0 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/nativeBridge.d.ts +0 -3
- package/dist/utils/nativeBridge.d.ts.map +0 -1
- package/dist/utils/projectTags.d.ts +0 -6
- package/dist/utils/projectTags.d.ts.map +0 -1
- package/dist/utils/slug.d.ts +0 -13
- package/dist/utils/slug.d.ts.map +0 -1
- package/dist/utils/telemetryError.d.ts +0 -2
- package/dist/utils/telemetryError.d.ts.map +0 -1
- package/dist/utils/tokenHelpers.d.ts +0 -114
- package/dist/utils/tokenHelpers.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemberListItem.js","names":[],"sources":["../../src/components/MemberListItem.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n Card,\n CardContent,\n Button,\n Badge,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger,\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\nimport { MoreHorizontal, UserMinus } from 'lucide-react';\nimport { useRemoveWorkspaceMember } from '../hooks/useMemberMutations';\nimport { nativeImpact, nativeNotify } from '../utils/nativeBridge';\nimport { formatRelativeTime } from '../utils/formatters';\nimport
|
|
1
|
+
{"version":3,"file":"MemberListItem.js","names":[],"sources":["../../src/components/MemberListItem.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Card,\n CardContent,\n Button,\n Badge,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuTrigger,\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n} from '@burdenoff/fe-libs/ui';\nimport { MoreHorizontal, UserMinus } from 'lucide-react';\nimport { ActorIdentity, useActorProfiles, type ActorInput } from '@burdenoff/fe-libs/shared';\nimport { useRemoveWorkspaceMember } from '../hooks/useMemberMutations';\nimport { nativeImpact, nativeNotify } from '../utils/nativeBridge';\nimport { formatRelativeTime } from '../utils/formatters';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport type { WorkspaceMember } from '../types';\n\nexport interface MemberListItemProps {\n member: WorkspaceMember;\n onUpdateRoles?: (memberId: string, roleIds: string[]) => void;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n canRemove?: boolean;\n}\n\nexport function MemberListItem({\n member,\n selected = false,\n onToggleSelect,\n canRemove = false,\n}: MemberListItemProps) {\n const [showRemoveDialog, setShowRemoveDialog] = useState(false);\n const { mutate: removeMember, isPending: isRemoving } = useRemoveWorkspaceMember(\n member.workspaceId\n );\n const { apiGatewayUrl, authToken, organizationId } = useWorkspacesContext();\n\n // Resolve userId → displayName + email via the shared actor profile cache.\n // UUIDs must never headline a row — the resolved identity goes in primary\n // position; the raw ID lives in the drill-down inside ActorIdentity.\n const actors = useMemo<ActorInput[]>(\n () => [{ actorId: member.userId, actorType: 'user' }],\n [member.userId]\n );\n const profileMap = useActorProfiles(actors, {\n apiGatewayUrl,\n authToken,\n orgId: organizationId,\n });\n const profile = profileMap[member.userId];\n\n const handleRemove = () => {\n void nativeImpact('medium');\n removeMember(member.id, {\n onSuccess: () => {\n void nativeNotify('success');\n setShowRemoveDialog(false);\n },\n onError: () => {\n void nativeNotify('error');\n },\n });\n };\n\n return (\n <>\n <Card\n className={`hover:bg-bg-canvas transition-all duration-200 ${\n selected ? 'ring-2 ring-action-primary-bg/25 border-action-primary-bg/50' : ''\n }`}\n >\n <CardContent className=\"p-4\">\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex items-center gap-3 flex-1 min-w-0\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n className=\"size-4 rounded border-border-default text-action-primary-bg\"\n aria-label={`Select member ${profile?.displayName ?? ''}`}\n />\n )}\n\n <div className=\"flex-1 min-w-0\">\n {profile ? <ActorIdentity profile={profile} variant=\"cell\" /> : null}\n <div className=\"mt-1 flex items-center gap-2\">\n <Badge variant=\"secondary\" className=\"text-xs\">\n Workspace member\n </Badge>\n <span className=\"text-xs text-text-secondary\">\n Joined {formatRelativeTime(member.createdAt)}\n </span>\n </div>\n </div>\n </div>\n\n {canRemove && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"size-8 flex-shrink-0\">\n <MoreHorizontal className=\"size-4\" />\n <span className=\"sr-only\">Open member actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <button\n type=\"button\"\n onClick={() => setShowRemoveDialog(true)}\n className=\"flex w-full items-center rounded-sm px-2 py-1.5 text-sm text-status-danger-text outline-none transition-colors hover:bg-action-ghost-bg-hover focus:bg-action-ghost-bg-hover\"\n >\n <UserMinus className=\"mr-2 size-4\" />\n Remove Member\n </button>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n </div>\n </CardContent>\n </Card>\n\n <AlertDialog open={showRemoveDialog} onOpenChange={setShowRemoveDialog}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Remove Member</AlertDialogTitle>\n <AlertDialogDescription>\n Are you sure you want to remove {profile?.displayName ?? 'this member'} from the\n workspace? This action cannot be undone.\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel disabled={isRemoving}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={handleRemove}\n disabled={isRemoving}\n className=\"bg-status-danger-bg text-status-danger-text hover:bg-status-danger-bg/90\"\n >\n {isRemoving ? 'Removing...' : 'Remove Member'}\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </>\n );\n}\n"],"mappings":";;;;;;;;;;AAkCA,SAAgB,EAAe,EAC7B,WACA,cAAW,IACX,mBACA,eAAY,MACU;CACtB,IAAM,CAAC,GAAkB,KAAuB,EAAS,GAAM,EACzD,EAAE,QAAQ,GAAc,WAAW,MAAe,EACtD,EAAO,YACR,EACK,EAAE,kBAAe,cAAW,sBAAmB,GAAsB,EAcrE,IALa,EAJJ,QACP,CAAC;EAAE,SAAS,EAAO;EAAQ,WAAW;EAAQ,CAAC,EACrD,CAAC,EAAO,OAAO,CAChB,EAC2C;EAC1C;EACA;EACA,OAAO;EACR,CAAC,CACyB,EAAO;AAelC,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;EACE,WAAW,kDACT,IAAW,iEAAiE;YAG9E,kBAAC,GAAD;GAAa,WAAU;aACrB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,KACC,kBAAC,SAAD;MACE,MAAK;MACL,SAAS;MACT,WAAW,MAAU,EAAe,EAAM,OAAO,QAAQ;MACzD,WAAU;MACV,cAAY,iBAAiB,GAAS,eAAe;MACrD,CAAA,EAGJ,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACG,IAAU,kBAAC,GAAD;OAAwB;OAAS,SAAQ;OAAS,CAAA,GAAG,MAChE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;QAAY,WAAU;kBAAU;QAEvC,CAAA,EACR,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAA8C,WACpC,EAAmB,EAAO,UAAU,CACvC;UACH;SACF;QACF;QAEL,KACC,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAqB,SAAA;eACnB,kBAAC,GAAD;MAAQ,SAAQ;MAAQ,MAAK;MAAO,WAAU;gBAA9C,CACE,kBAAC,GAAD,EAAgB,WAAU,UAAW,CAAA,EACrC,kBAAC,QAAD;OAAM,WAAU;iBAAU;OAA0B,CAAA,CAC7C;;KACW,CAAA,EACtB,kBAAC,GAAD;KAAqB,OAAM;eACzB,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAoB,GAAK;MACxC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA,EAAA,gBAE9B;;KACW,CAAA,CACT,EAAA,CAAA,CAEb;;GACM,CAAA;EACT,CAAA,EAEP,kBAAC,GAAD;EAAa,MAAM;EAAkB,cAAc;YACjD,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAkB,iBAAgC,CAAA,EAClD,kBAAC,GAAD,EAAA,UAAA;GAAwB;GACW,GAAS,eAAe;GAAc;GAEhD,EAAA,CAAA,CACP,EAAA,CAAA,EACpB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;GAAmB,UAAU;aAAY;GAA0B,CAAA,EACnE,kBAAC,GAAD;GACE,eAlFe;AAEzB,IADK,EAAa,SAAS,EAC3B,EAAa,EAAO,IAAI;KACtB,iBAAiB;AAEf,MADK,EAAa,UAAU,EAC5B,EAAoB,GAAM;;KAE5B,eAAe;AACR,QAAa,QAAQ;;KAE7B,CAAC;;GAyEQ,UAAU;GACV,WAAU;aAET,IAAa,gBAAgB;GACZ,CAAA,CACF,EAAA,CAAA,CACD,EAAA,CAAA;EACT,CAAA,CACb,EAAA,CAAA"}
|
|
@@ -35,7 +35,7 @@ function D({ project: D, selected: O = !1, onToggleSelect: k, onTagClick: A, can
|
|
|
35
35
|
onChange: (e) => k(e.target.checked),
|
|
36
36
|
onClick: (e) => e.stopPropagation(),
|
|
37
37
|
"aria-label": `Select project ${D.name}`,
|
|
38
|
-
className: "mt-1
|
|
38
|
+
className: "mt-1 size-4 rounded border-border text-primary"
|
|
39
39
|
}),
|
|
40
40
|
/* @__PURE__ */ p("div", {
|
|
41
41
|
className: "p-3 rounded-lg bg-primary/10 transition-colors",
|
|
@@ -61,7 +61,7 @@ function D({ project: D, selected: O = !1, onToggleSelect: k, onTagClick: A, can
|
|
|
61
61
|
children: /* @__PURE__ */ m(g, {
|
|
62
62
|
variant: "ghost",
|
|
63
63
|
size: "sm",
|
|
64
|
-
className: "
|
|
64
|
+
className: "size-8 p-0",
|
|
65
65
|
children: [/* @__PURE__ */ p(f, { size: 16 }), /* @__PURE__ */ p("span", {
|
|
66
66
|
className: "sr-only",
|
|
67
67
|
children: "Open project actions"
|
|
@@ -117,14 +117,16 @@ function D({ project: D, selected: O = !1, onToggleSelect: k, onTagClick: A, can
|
|
|
117
117
|
className: "flex flex-wrap gap-1.5",
|
|
118
118
|
"aria-label": "Project tags",
|
|
119
119
|
children: [z.slice(0, 4).map((e) => /* @__PURE__ */ p("span", {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
...A ? {
|
|
121
|
+
role: "button",
|
|
122
|
+
tabIndex: 0,
|
|
123
|
+
onClick: (t) => {
|
|
124
|
+
t.stopPropagation(), A(e);
|
|
125
|
+
},
|
|
126
|
+
onKeyDown: (t) => {
|
|
127
|
+
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), t.stopPropagation(), A(e));
|
|
128
|
+
}
|
|
129
|
+
} : {},
|
|
128
130
|
children: /* @__PURE__ */ p(E, {
|
|
129
131
|
id: e.tagId,
|
|
130
132
|
label: e.label,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectCard.js","names":[],"sources":["../../src/components/ProjectCard.tsx"],"sourcesContent":["import { FolderKanban, Archive, MoreVertical } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\n\nimport type { Project, ProjectTag } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport { getProjectStatusClasses } from '../utils/tokenHelpers';\nimport {\n useArchiveProject,\n useUnarchiveProject,\n useDeleteProject,\n} from '../hooks/useProjectMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\n\ninterface ProjectCardProps {\n project: Project;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n onTagClick?: (tag: ProjectTag) => void;\n canArchive?: boolean;\n canDelete?: boolean;\n}\n\nexport function ProjectCard({\n project,\n selected = false,\n onToggleSelect,\n onTagClick,\n canArchive = false,\n canDelete = false,\n}: ProjectCardProps) {\n const navigate = useNavigate();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { mutate: archiveProject } = useArchiveProject(project.workspaceId);\n const { mutate: unarchiveProject } = useUnarchiveProject(project.workspaceId);\n const { mutate: deleteProject } = useDeleteProject(project.workspaceId);\n const projectTags = normalizeProjectTags(project.tags);\n\n const handleCardClick = () => {\n navigate(withCurrentSearch(`${basePath}/${project.workspaceId}/projects/${project.id}`));\n };\n\n const handleArchive = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (project.archived) {\n unarchiveProject(project.id);\n } else {\n archiveProject(project.id);\n }\n };\n\n const handleDelete = async (e: React.MouseEvent) => {\n e.stopPropagation();\n void nativeImpact('medium');\n const native = await nativeConfirm({\n title: 'Delete project',\n message: `Are you sure you want to delete \"${project.name}\"?`,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const ok =\n native === null ? confirm(`Are you sure you want to delete \"${project.name}\"?`) : native;\n if (ok) {\n deleteProject(project.id);\n }\n };\n\n const handleCardKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleCardClick();\n }\n };\n\n return (\n <Card\n className={`cursor-pointer relative ${\n selected ? 'ring-2 ring-primary/25 border-primary/50' : ''\n }`}\n onClick={handleCardClick}\n onKeyDown={handleCardKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open project ${project.name}`}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3 flex-1\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n onClick={(event) => event.stopPropagation()}\n aria-label={`Select project ${project.name}`}\n className=\"mt-1 h-4 w-4 rounded border-border text-primary\"\n />\n )}\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <FolderKanban className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"truncate\">{project.name}</CardTitle>\n <p className=\"truncate text-sm text-muted-foreground\">{project.key}</p>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n <Button variant=\"ghost\" size=\"sm\" className=\"h-8 w-8 p-0\">\n <MoreVertical size={16} />\n <span className=\"sr-only\">Open project actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {canArchive && (\n <DropdownMenuItem onClick={handleArchive}>\n <Archive size={16} className=\"mr-2\" />\n {project.archived ? 'Unarchive' : 'Archive'}\n </DropdownMenuItem>\n )}\n {canDelete && (\n <DropdownMenuItem onClick={handleDelete} className=\"text-destructive\">\n Delete\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Badge className={`border ${getProjectStatusClasses(project.status)}`}>\n {project.status}\n </Badge>\n {project.archived && (\n <Badge\n variant=\"outline\"\n className=\"border-workspace-archived-border text-workspace-archived-text\"\n >\n <Archive size={12} className=\"mr-1\" />\n Archived\n </Badge>\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">Created {formatDate(project.createdAt)}</div>\n {projectTags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Project tags\">\n {projectTags.slice(0, 4).map((tag) => (\n <span\n key={tag.tagId}\n onClick={(event) => {\n event.stopPropagation();\n onTagClick?.(tag);\n }}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n onTagClick?.(tag);\n }\n }}\n role={onTagClick ? 'button' : undefined}\n tabIndex={onTagClick ? 0 : -1}\n >\n <TagBadge\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n size=\"sm\"\n className={onTagClick ? 'cursor-pointer' : undefined}\n />\n </span>\n ))}\n {projectTags.length > 4 && (\n <Badge variant=\"outline\" className=\"text-xs\">\n +{projectTags.length - 4}\n </Badge>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProjectCard.js","names":[],"sources":["../../src/components/ProjectCard.tsx"],"sourcesContent":["import type React from 'react';\nimport { FolderKanban, Archive, MoreVertical } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\n\nimport type { Project, ProjectTag } from '../types';\nimport { formatDate } from '../utils/formatters';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport { getProjectStatusClasses } from '../utils/tokenHelpers';\nimport {\n useArchiveProject,\n useUnarchiveProject,\n useDeleteProject,\n} from '../hooks/useProjectMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\n\ninterface ProjectCardProps {\n project: Project;\n selected?: boolean;\n onToggleSelect?: (checked: boolean) => void;\n onTagClick?: (tag: ProjectTag) => void;\n canArchive?: boolean;\n canDelete?: boolean;\n}\n\nexport function ProjectCard({\n project,\n selected = false,\n onToggleSelect,\n onTagClick,\n canArchive = false,\n canDelete = false,\n}: ProjectCardProps) {\n const navigate = useNavigate();\n const { basePath = '/workspaces' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n const { mutate: archiveProject } = useArchiveProject(project.workspaceId);\n const { mutate: unarchiveProject } = useUnarchiveProject(project.workspaceId);\n const { mutate: deleteProject } = useDeleteProject(project.workspaceId);\n const projectTags = normalizeProjectTags(project.tags);\n\n const handleCardClick = () => {\n navigate(withCurrentSearch(`${basePath}/${project.workspaceId}/projects/${project.id}`));\n };\n\n const handleArchive = (e: React.MouseEvent) => {\n e.stopPropagation();\n if (project.archived) {\n unarchiveProject(project.id);\n } else {\n archiveProject(project.id);\n }\n };\n\n const handleDelete = async (e: React.MouseEvent) => {\n e.stopPropagation();\n void nativeImpact('medium');\n const native = await nativeConfirm({\n title: 'Delete project',\n message: `Are you sure you want to delete \"${project.name}\"?`,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const ok =\n native === null ? confirm(`Are you sure you want to delete \"${project.name}\"?`) : native;\n if (ok) {\n deleteProject(project.id);\n }\n };\n\n const handleCardKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n handleCardClick();\n }\n };\n\n return (\n <Card\n className={`cursor-pointer relative ${\n selected ? 'ring-2 ring-primary/25 border-primary/50' : ''\n }`}\n onClick={handleCardClick}\n onKeyDown={handleCardKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open project ${project.name}`}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3 flex-1\">\n {onToggleSelect && (\n <input\n type=\"checkbox\"\n checked={selected}\n onChange={(event) => onToggleSelect(event.target.checked)}\n onClick={(event) => event.stopPropagation()}\n aria-label={`Select project ${project.name}`}\n className=\"mt-1 size-4 rounded border-border text-primary\"\n />\n )}\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <FolderKanban className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"truncate\">{project.name}</CardTitle>\n <p className=\"truncate text-sm text-muted-foreground\">{project.key}</p>\n </div>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n <Button variant=\"ghost\" size=\"sm\" className=\"size-8 p-0\">\n <MoreVertical size={16} />\n <span className=\"sr-only\">Open project actions</span>\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n {canArchive && (\n <DropdownMenuItem onClick={handleArchive}>\n <Archive size={16} className=\"mr-2\" />\n {project.archived ? 'Unarchive' : 'Archive'}\n </DropdownMenuItem>\n )}\n {canDelete && (\n <DropdownMenuItem onClick={handleDelete} className=\"text-destructive\">\n Delete\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Badge className={`border ${getProjectStatusClasses(project.status)}`}>\n {project.status}\n </Badge>\n {project.archived && (\n <Badge\n variant=\"outline\"\n className=\"border-workspace-archived-border text-workspace-archived-text\"\n >\n <Archive size={12} className=\"mr-1\" />\n Archived\n </Badge>\n )}\n </div>\n <div className=\"text-xs text-muted-foreground\">Created {formatDate(project.createdAt)}</div>\n {projectTags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\" aria-label=\"Project tags\">\n {projectTags.slice(0, 4).map((tag) => (\n <span\n key={tag.tagId}\n {...(onTagClick\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: (event: React.MouseEvent<HTMLSpanElement>) => {\n event.stopPropagation();\n onTagClick(tag);\n },\n onKeyDown: (event: React.KeyboardEvent<HTMLSpanElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n event.stopPropagation();\n onTagClick(tag);\n }\n },\n }\n : {})}\n >\n <TagBadge\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n size=\"sm\"\n className={onTagClick ? 'cursor-pointer' : undefined}\n />\n </span>\n ))}\n {projectTags.length > 4 && (\n <Badge variant=\"outline\" className=\"text-xs\">\n +{projectTags.length - 4}\n </Badge>\n )}\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspaceCard.js","names":[],"sources":["../../src/components/WorkspaceCard.tsx"],"sourcesContent":["import { Building2, Users, FolderKanban } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { Card, CardHeader, CardTitle, CardContent, Badge } from '@burdenoff/fe-libs/ui';\nimport type { Workspace } from '../types';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { getWorkspaceStatusClasses } from '../utils/tokenHelpers';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\n\ninterface WorkspaceCardProps {\n workspace: Workspace;\n}\n\nexport function WorkspaceCard({ workspace }: WorkspaceCardProps) {\n const navigate = useNavigate();\n const { basePath = '/' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n\n const
|
|
1
|
+
{"version":3,"file":"WorkspaceCard.js","names":[],"sources":["../../src/components/WorkspaceCard.tsx"],"sourcesContent":["import { Building2, Users, FolderKanban } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { Card, CardHeader, CardTitle, CardContent, Badge } from '@burdenoff/fe-libs/ui';\nimport type { Workspace } from '../types';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { getWorkspaceStatusClasses } from '../utils/tokenHelpers';\nimport { useScopedNavigation } from '../hooks/useScopedNavigation';\n\ninterface WorkspaceCardProps {\n workspace: Workspace;\n}\n\nexport function WorkspaceCard({ workspace }: WorkspaceCardProps) {\n const navigate = useNavigate();\n const { basePath = '/' } = useWorkspacesContext();\n const { withCurrentSearch } = useScopedNavigation();\n\n const openWorkspace = () => {\n navigate(withCurrentSearch(`${basePath}/${workspace.id}`));\n };\n\n const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n openWorkspace();\n }\n };\n\n return (\n <Card\n className=\"cursor-pointer\"\n onClick={openWorkspace}\n onKeyDown={handleKeyDown}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open workspace ${workspace.name}`}\n >\n <CardHeader>\n <div className=\"flex items-center gap-3\">\n <div className=\"p-3 rounded-lg bg-primary/10 transition-colors\">\n <Building2 className=\"text-primary\" size={24} />\n </div>\n <div className=\"flex-1 min-w-0\">\n <CardTitle className=\"text-lg truncate\">{workspace.name}</CardTitle>\n <div className=\"flex items-center gap-2 mt-1\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {workspace.type}\n </Badge>\n <Badge className={`text-xs border ${getWorkspaceStatusClasses(workspace.status)}`}>\n {workspace.status}\n </Badge>\n </div>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"flex flex-wrap gap-4 text-sm text-muted-foreground\">\n {typeof workspace.memberCount === 'number' && workspace.memberCount > 0 && (\n <div className=\"flex items-center gap-1\">\n <Users size={16} aria-hidden=\"true\" />\n <span>\n {workspace.memberCount} member{workspace.memberCount === 1 ? '' : 's'}\n </span>\n </div>\n )}\n {typeof workspace.projectCount === 'number' && workspace.projectCount > 0 && (\n <div className=\"flex items-center gap-1\">\n <FolderKanban size={16} aria-hidden=\"true\" />\n <span>\n {workspace.projectCount} project{workspace.projectCount === 1 ? '' : 's'}\n </span>\n </div>\n )}\n </div>\n {workspace.domain && (\n <p className=\"mt-2 truncate text-xs text-muted-foreground\">{workspace.domain}</p>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":";;;;;;;;AAYA,SAAgB,EAAc,EAAE,gBAAiC;CAC/D,IAAM,IAAW,GAAa,EACxB,EAAE,cAAW,QAAQ,GAAsB,EAC3C,EAAE,yBAAsB,GAAqB,EAE7C,UAAsB;AAC1B,IAAS,EAAkB,GAAG,EAAS,GAAG,EAAU,KAAK,CAAC;;AAU5D,QACE,kBAAC,GAAD;EACE,WAAU;EACV,SAAS;EACT,YAXmB,MAA+C;AACpE,IAAI,EAAM,QAAQ,WAAW,EAAM,QAAQ,SACzC,EAAM,gBAAgB,EACtB,GAAe;;EASf,MAAK;EACL,UAAU;EACV,cAAY,kBAAkB,EAAU;YAN1C,CAQE,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,WAAU;KAAe,MAAM;KAAM,CAAA;IAC5C,CAAA,EACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAW,WAAU;eAAoB,EAAU;KAAiB,CAAA,EACpE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,SAAQ;MAAU,WAAU;gBAChC,EAAU;MACL,CAAA,EACR,kBAAC,GAAD;MAAO,WAAW,kBAAkB,EAA0B,EAAU,OAAO;gBAC5E,EAAU;MACL,CAAA,CACJ;OACF;MACF;MACK,CAAA,EACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACG,OAAO,EAAU,eAAgB,YAAY,EAAU,cAAc,KACpE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAO,MAAM;KAAI,eAAY;KAAS,CAAA,EACtC,kBAAC,QAAD,EAAA,UAAA;KACG,EAAU;KAAY;KAAQ,EAAU,gBAAgB,IAAI,KAAK;KAC7D,EAAA,CAAA,CACH;OAEP,OAAO,EAAU,gBAAiB,YAAY,EAAU,eAAe,KACtE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAc,MAAM;KAAI,eAAY;KAAS,CAAA,EAC7C,kBAAC,QAAD,EAAA,UAAA;KACG,EAAU;KAAa;KAAS,EAAU,iBAAiB,IAAI,KAAK;KAChE,EAAA,CAAA,CACH;MAEJ;MACL,EAAU,UACT,kBAAC,KAAD;GAAG,WAAU;aAA+C,EAAU;GAAW,CAAA,CAEvE,EAAA,CAAA,CACT"}
|
|
@@ -45,6 +45,14 @@ var s = class extends e {
|
|
|
45
45
|
window.location.reload();
|
|
46
46
|
};
|
|
47
47
|
handleGoHome = () => {
|
|
48
|
+
if (this.props.onGoHome) {
|
|
49
|
+
this.props.onGoHome(), this.setState({
|
|
50
|
+
hasError: !1,
|
|
51
|
+
error: null,
|
|
52
|
+
errorInfo: null
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
48
56
|
window.location.href = "/";
|
|
49
57
|
};
|
|
50
58
|
render() {
|
|
@@ -57,7 +65,7 @@ var s = class extends e {
|
|
|
57
65
|
className: "flex justify-center",
|
|
58
66
|
children: /* @__PURE__ */ i("div", {
|
|
59
67
|
className: "rounded-full bg-destructive/10 p-6",
|
|
60
|
-
children: /* @__PURE__ */ i(t, { className: "
|
|
68
|
+
children: /* @__PURE__ */ i(t, { className: "size-12 text-destructive" })
|
|
61
69
|
})
|
|
62
70
|
}),
|
|
63
71
|
/* @__PURE__ */ a("div", {
|
|
@@ -99,7 +107,7 @@ var s = class extends e {
|
|
|
99
107
|
/* @__PURE__ */ a(o, {
|
|
100
108
|
onClick: this.handleReset,
|
|
101
109
|
variant: "default",
|
|
102
|
-
children: [/* @__PURE__ */ i(r, { className: "mr-2
|
|
110
|
+
children: [/* @__PURE__ */ i(r, { className: "mr-2 size-4" }), "Try Again"]
|
|
103
111
|
}),
|
|
104
112
|
/* @__PURE__ */ i(o, {
|
|
105
113
|
onClick: this.handleReload,
|
|
@@ -109,7 +117,7 @@ var s = class extends e {
|
|
|
109
117
|
/* @__PURE__ */ a(o, {
|
|
110
118
|
onClick: this.handleGoHome,
|
|
111
119
|
variant: "ghost",
|
|
112
|
-
children: [/* @__PURE__ */ i(n, { className: "mr-2
|
|
120
|
+
children: [/* @__PURE__ */ i(n, { className: "mr-2 size-4" }), "Go Home"]
|
|
113
121
|
})
|
|
114
122
|
]
|
|
115
123
|
}),
|
|
@@ -119,7 +127,7 @@ var s = class extends e {
|
|
|
119
127
|
"Need help?",
|
|
120
128
|
" ",
|
|
121
129
|
/* @__PURE__ */ i("a", {
|
|
122
|
-
href: "
|
|
130
|
+
href: `mailto:${this.props.supportEmail ?? "support@burdenoff.com"}`,
|
|
123
131
|
className: "text-primary hover:underline transition-colors duration-200",
|
|
124
132
|
children: "Contact Support"
|
|
125
133
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspacesErrorBoundary.js","names":[],"sources":["../../src/components/WorkspacesErrorBoundary.tsx"],"sourcesContent":["import { Component, type ReactNode } from 'react';\nimport { AlertCircle, RefreshCw, Home } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n onReset?: () => void;\n}\n\ninterface State {\n hasError: boolean;\n error: Error | null;\n errorInfo: React.ErrorInfo | null;\n}\n\n/**\n * Error Boundary component for Workspaces microfrontend\n * Catches and handles runtime errors gracefully\n */\nexport class WorkspacesErrorBoundary extends Component<Props, State> {\n constructor(props: Props) {\n super(props);\n this.state = {\n hasError: false,\n error: null,\n errorInfo: null,\n };\n }\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n // Log error to console in development\n if (process.env.NODE_ENV === 'development') {\n console.error('Workspaces Error Boundary caught an error:', error, errorInfo);\n }\n\n // Update state with error details\n this.setState({\n error,\n errorInfo,\n });\n\n // Forward to the shell-owned event bus per\n // ~/products/infra/infra-specs/microfrontends/47-FRONTEND-EVENT-BUS.md.\n // The shell wires Rybbit / OTel / SRE sinks; the MFE never sinks directly.\n // Window guard keeps SSR / non-browser environments safe.\n if (typeof window !== 'undefined') {\n const w = window as unknown as {\n __burdenoffEventBus?: { emit: (name: string, payload: unknown) => void };\n };\n try {\n w.__burdenoffEventBus?.emit('workspaces.error-boundary.caught', {\n source: 'microfe-workspaces',\n message: error.message,\n name: error.name,\n stack: error.stack,\n componentStack: errorInfo.componentStack,\n });\n } catch {\n // Bus failures must not mask the original render error.\n }\n }\n }\n\n handleReset = () => {\n this.setState({\n hasError: false,\n error: null,\n errorInfo: null,\n });\n\n // Call optional onReset callback\n this.props.onReset?.();\n };\n\n handleReload = () => {\n window.location.reload();\n };\n\n handleGoHome = () => {\n window.location.href = '/';\n };\n\n render() {\n if (this.state.hasError) {\n // Use custom fallback if provided\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n // Default error UI\n return (\n <div className=\"flex items-center justify-center min-h-[400px] p-6\">\n <div className=\"max-w-md w-full text-center space-y-6\">\n {/* Error Icon */}\n <div className=\"flex justify-center\">\n <div className=\"rounded-full bg-destructive/10 p-6\">\n <AlertCircle className=\"
|
|
1
|
+
{"version":3,"file":"WorkspacesErrorBoundary.js","names":[],"sources":["../../src/components/WorkspacesErrorBoundary.tsx"],"sourcesContent":["import { Component, type ReactNode } from 'react';\nimport { AlertCircle, RefreshCw, Home } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\ninterface Props {\n children: ReactNode;\n fallback?: ReactNode;\n onReset?: () => void;\n /**\n * Optional \"go home\" handler. The boundary is a class component (can't\n * call `useNavigate`), so the host shell injects a React-Router-aware\n * handler here. Falls back to `window.location.href = '/'` when omitted\n * so a host that hasn't wired this up still gets a working button.\n */\n onGoHome?: () => void;\n /**\n * Contact-support email shown in the fallback UI. Defaults to\n * `support@burdenoff.com`; hosts can override (e.g. for a tenant-branded\n * deployment).\n */\n supportEmail?: string;\n}\n\ninterface State {\n hasError: boolean;\n error: Error | null;\n errorInfo: React.ErrorInfo | null;\n}\n\n/**\n * Error Boundary component for Workspaces microfrontend\n * Catches and handles runtime errors gracefully\n */\nexport class WorkspacesErrorBoundary extends Component<Props, State> {\n constructor(props: Props) {\n super(props);\n this.state = {\n hasError: false,\n error: null,\n errorInfo: null,\n };\n }\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n // Log error to console in development\n if (process.env.NODE_ENV === 'development') {\n console.error('Workspaces Error Boundary caught an error:', error, errorInfo);\n }\n\n // Update state with error details\n this.setState({\n error,\n errorInfo,\n });\n\n // Forward to the shell-owned event bus per\n // ~/products/infra/infra-specs/microfrontends/47-FRONTEND-EVENT-BUS.md.\n // The shell wires Rybbit / OTel / SRE sinks; the MFE never sinks directly.\n // Window guard keeps SSR / non-browser environments safe.\n if (typeof window !== 'undefined') {\n const w = window as unknown as {\n __burdenoffEventBus?: { emit: (name: string, payload: unknown) => void };\n };\n try {\n w.__burdenoffEventBus?.emit('workspaces.error-boundary.caught', {\n source: 'microfe-workspaces',\n message: error.message,\n name: error.name,\n stack: error.stack,\n componentStack: errorInfo.componentStack,\n });\n } catch {\n // Bus failures must not mask the original render error.\n }\n }\n }\n\n handleReset = () => {\n this.setState({\n hasError: false,\n error: null,\n errorInfo: null,\n });\n\n // Call optional onReset callback\n this.props.onReset?.();\n };\n\n handleReload = () => {\n // Deliberately a full-page reload here: the React tree threw and is\n // already corrupted, so SPA navigation can't be trusted. This is the\n // one window.location call we keep in the boundary.\n window.location.reload();\n };\n\n handleGoHome = () => {\n if (this.props.onGoHome) {\n this.props.onGoHome();\n // SPA navigation doesn't remount this boundary; clear error state so\n // the new route renders instead of the fallback UI.\n this.setState({ hasError: false, error: null, errorInfo: null });\n return;\n }\n window.location.href = '/';\n };\n\n render() {\n if (this.state.hasError) {\n // Use custom fallback if provided\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n // Default error UI\n return (\n <div className=\"flex items-center justify-center min-h-[400px] p-6\">\n <div className=\"max-w-md w-full text-center space-y-6\">\n {/* Error Icon */}\n <div className=\"flex justify-center\">\n <div className=\"rounded-full bg-destructive/10 p-6\">\n <AlertCircle className=\"size-12 text-destructive\" />\n </div>\n </div>\n\n {/* Error Message */}\n <div className=\"space-y-2\">\n <h2 className=\"text-2xl font-bold\">Something went wrong</h2>\n <p className=\"text-muted-foreground\">\n We encountered an unexpected error. Please try refreshing the page or contact\n support if the problem persists.\n </p>\n </div>\n\n {/* Error Details (Development Only) */}\n {process.env.NODE_ENV === 'development' && this.state.error && (\n <details className=\"text-left\">\n <summary className=\"cursor-pointer text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-200\">\n Error Details\n </summary>\n <div className=\"mt-2 p-4 bg-muted rounded-lg text-xs font-mono overflow-auto max-h-40\">\n <p className=\"font-semibold text-destructive\">{this.state.error.name}:</p>\n <p className=\"text-foreground\">{this.state.error.message}</p>\n {this.state.error.stack && (\n <pre className=\"mt-2 text-muted-foreground text-xs overflow-x-auto\">\n {this.state.error.stack}\n </pre>\n )}\n </div>\n </details>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex flex-col sm:flex-row gap-3 justify-center\">\n <Button onClick={this.handleReset} variant=\"default\">\n <RefreshCw className=\"mr-2 size-4\" />\n Try Again\n </Button>\n <Button onClick={this.handleReload} variant=\"outline\">\n Reload Page\n </Button>\n <Button onClick={this.handleGoHome} variant=\"ghost\">\n <Home className=\"mr-2 size-4\" />\n Go Home\n </Button>\n </div>\n\n {/* Support Link */}\n <p className=\"text-sm text-muted-foreground\">\n Need help?{' '}\n <a\n href={`mailto:${this.props.supportEmail ?? 'support@burdenoff.com'}`}\n className=\"text-primary hover:underline transition-colors duration-200\"\n >\n Contact Support\n </a>\n </p>\n </div>\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n"],"mappings":";;;;;AAiCA,IAAa,IAAb,cAA6C,EAAwB;CACnE,YAAY,GAAc;AAExB,EADA,MAAM,EAAM,EACZ,KAAK,QAAQ;GACX,UAAU;GACV,OAAO;GACP,WAAW;GACZ;;CAGH,OAAO,yBAAyB,GAA8B;AAC5D,SAAO;GAAE,UAAU;GAAM;GAAO;;CAGlC,kBAAkB,GAAc,GAA4B;AAgB1D,MAdA,QAAA,IAAA,aAA6B,iBAC3B,QAAQ,MAAM,8CAA8C,GAAO,EAAU,EAI/E,KAAK,SAAS;GACZ;GACA;GACD,CAAC,EAME,OAAO,SAAW,KAAa;GACjC,IAAM,IAAI;AAGV,OAAI;AACF,MAAE,qBAAqB,KAAK,oCAAoC;KAC9D,QAAQ;KACR,SAAS,EAAM;KACf,MAAM,EAAM;KACZ,OAAO,EAAM;KACb,gBAAgB,EAAU;KAC3B,CAAC;WACI;;;CAMZ,oBAAoB;AAQlB,EAPA,KAAK,SAAS;GACZ,UAAU;GACV,OAAO;GACP,WAAW;GACZ,CAAC,EAGF,KAAK,MAAM,WAAW;;CAGxB,qBAAqB;AAInB,SAAO,SAAS,QAAQ;;CAG1B,qBAAqB;AACnB,MAAI,KAAK,MAAM,UAAU;AAIvB,GAHA,KAAK,MAAM,UAAU,EAGrB,KAAK,SAAS;IAAE,UAAU;IAAO,OAAO;IAAM,WAAW;IAAM,CAAC;AAChE;;AAEF,SAAO,SAAS,OAAO;;CAGzB,SAAS;AA2EP,SA1EI,KAAK,MAAM,WAET,KAAK,MAAM,WACN,KAAK,MAAM,WAKlB,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD;IAAK,WAAU;cAAf;KAEE,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD,EAAa,WAAU,4BAA6B,CAAA;OAChD,CAAA;MACF,CAAA;KAGN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAqB;OAAyB,CAAA,EAC5D,kBAAC,KAAD;OAAG,WAAU;iBAAwB;OAGjC,CAAA,CACA;;8BAGoB,iBAAiB,KAAK,MAAM,SACpD,kBAAC,WAAD;MAAS,WAAU;gBAAnB,CACE,kBAAC,WAAD;OAAS,WAAU;iBAAgH;OAEzH,CAAA,EACV,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,KAAD;SAAG,WAAU;mBAAb,CAA+C,KAAK,MAAM,MAAM,MAAK,IAAK;;QAC1E,kBAAC,KAAD;SAAG,WAAU;mBAAmB,KAAK,MAAM,MAAM;SAAY,CAAA;QAC5D,KAAK,MAAM,MAAM,SAChB,kBAAC,OAAD;SAAK,WAAU;mBACZ,KAAK,MAAM,MAAM;SACd,CAAA;QAEJ;SACE;;KAIZ,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD;QAAQ,SAAS,KAAK;QAAa,SAAQ;kBAA3C,CACE,kBAAC,GAAD,EAAW,WAAU,eAAgB,CAAA,EAAA,YAE9B;;OACT,kBAAC,GAAD;QAAQ,SAAS,KAAK;QAAc,SAAQ;kBAAU;QAE7C,CAAA;OACT,kBAAC,GAAD;QAAQ,SAAS,KAAK;QAAc,SAAQ;kBAA5C,CACE,kBAAC,GAAD,EAAM,WAAU,eAAgB,CAAA,EAAA,UAEzB;;OACL;;KAGN,kBAAC,KAAD;MAAG,WAAU;gBAAb;OAA6C;OAChC;OACX,kBAAC,KAAD;QACE,MAAM,UAAU,KAAK,MAAM,gBAAgB;QAC3C,WAAU;kBACX;QAEG,CAAA;OACF;;KACA;;GACF,CAAA,GAIH,KAAK,MAAM"}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { useCallback as e, useEffect as t, useRef as n, useState as r } from "react";
|
|
2
|
+
import { Loader2 as i, Plus as a, Search as ee, Tag as te, X as o } from "lucide-react";
|
|
3
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
4
|
+
import { Command as l, CommandEmpty as u, CommandGroup as d, CommandInput as f, CommandItem as p, CommandList as m, Popover as h, PopoverContent as ne, PopoverTrigger as re } from "@burdenoff/fe-libs/ui";
|
|
5
|
+
import { cn as g } from "@burdenoff/fe-libs/shared/utils";
|
|
6
|
+
import { useGatewayClient as ie } from "@burdenoff/fe-libs/shared/graphql";
|
|
7
|
+
import { gql as _ } from "@apollo/client";
|
|
8
|
+
import { useLazyQuery as v, useMutation as y } from "@apollo/client/react";
|
|
9
|
+
import { TagBadge as ae } from "@burdenoff/fe-libs/tag";
|
|
10
|
+
//#region src/components/tags/ProjectTagsWidget.tsx
|
|
11
|
+
var oe = _`
|
|
12
|
+
query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {
|
|
13
|
+
searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {
|
|
14
|
+
id
|
|
15
|
+
key
|
|
16
|
+
label
|
|
17
|
+
value
|
|
18
|
+
vocabularyId
|
|
19
|
+
parentId
|
|
20
|
+
hierarchyPath
|
|
21
|
+
color
|
|
22
|
+
icon
|
|
23
|
+
description
|
|
24
|
+
vocabulary {
|
|
25
|
+
key
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`, b = _`
|
|
30
|
+
query ProjectTagsWidget_GetVocabularyByKey($key: String!) {
|
|
31
|
+
getVocabularyByKey(key: $key) {
|
|
32
|
+
id
|
|
33
|
+
key
|
|
34
|
+
name
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
`, x = _`
|
|
38
|
+
mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {
|
|
39
|
+
createVocabulary(input: $input) {
|
|
40
|
+
id
|
|
41
|
+
key
|
|
42
|
+
name
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`, se = _`
|
|
46
|
+
mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {
|
|
47
|
+
createTag(input: $input) {
|
|
48
|
+
id
|
|
49
|
+
key
|
|
50
|
+
label
|
|
51
|
+
value
|
|
52
|
+
vocabularyId
|
|
53
|
+
parentId
|
|
54
|
+
hierarchyPath
|
|
55
|
+
color
|
|
56
|
+
icon
|
|
57
|
+
vocabulary {
|
|
58
|
+
key
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
`, S = "general";
|
|
63
|
+
function C({ entityType: _, entityId: C, workspaceId: w, tags: T, onAdd: E, onRemove: D, allowCreate: O = !0, maxTags: k = 20, className: ce }) {
|
|
64
|
+
let [le, A] = r(!1), [j, M] = r(""), [N, P] = r(null), [F, I] = r(!1), [L, R] = r(), z = n(null), B = ie("workspace"), V = !w || !C, H = new Set(T.map((e) => e.id)), U = T.length < k, [W, { data: ue, loading: G }] = v(oe, { client: B }), K = (ue?.searchTags ?? []).filter((e) => !H.has(e.id)), [q, { loading: J }] = y(se, { client: B }), [Y] = v(b, { client: B }), [X] = y(x, { client: B }), Z = e(async () => {
|
|
65
|
+
if (L) return L;
|
|
66
|
+
let e = (await Y({ variables: { key: S } })).data?.getVocabularyByKey;
|
|
67
|
+
if (e) return R(e.id), e.id;
|
|
68
|
+
let t = (await X({ variables: { input: {
|
|
69
|
+
key: S,
|
|
70
|
+
name: "General",
|
|
71
|
+
description: "Default vocabulary for general-purpose tags",
|
|
72
|
+
scope: "WORKSPACE",
|
|
73
|
+
allowCustomTags: !0
|
|
74
|
+
} } })).data?.createVocabulary;
|
|
75
|
+
if (!t) throw Error("Failed to create default vocabulary");
|
|
76
|
+
return R(t.id), t.id;
|
|
77
|
+
}, [
|
|
78
|
+
L,
|
|
79
|
+
Y,
|
|
80
|
+
X
|
|
81
|
+
]), de = e((e) => {
|
|
82
|
+
M(e), N && P(null), z.current && clearTimeout(z.current), e.trim() && (z.current = setTimeout(() => {
|
|
83
|
+
W({ variables: {
|
|
84
|
+
query: e,
|
|
85
|
+
vocabularyId: L,
|
|
86
|
+
limit: 10
|
|
87
|
+
} });
|
|
88
|
+
}, 250));
|
|
89
|
+
}, [
|
|
90
|
+
W,
|
|
91
|
+
L,
|
|
92
|
+
N
|
|
93
|
+
]);
|
|
94
|
+
t(() => () => {
|
|
95
|
+
z.current && clearTimeout(z.current);
|
|
96
|
+
}, []);
|
|
97
|
+
let Q = e(async (e) => {
|
|
98
|
+
if (!(!U || F)) {
|
|
99
|
+
I(!0);
|
|
100
|
+
try {
|
|
101
|
+
await E({
|
|
102
|
+
...e,
|
|
103
|
+
vocabularyKey: e.vocabularyKey ?? e.vocabulary?.key ?? null
|
|
104
|
+
}), M(""), A(!1);
|
|
105
|
+
} finally {
|
|
106
|
+
I(!1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}, [
|
|
110
|
+
E,
|
|
111
|
+
U,
|
|
112
|
+
F
|
|
113
|
+
]), fe = e(async () => {
|
|
114
|
+
if (!(!O || !j.trim() || J)) {
|
|
115
|
+
P(null);
|
|
116
|
+
try {
|
|
117
|
+
let e = j.trim(), t = e.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
118
|
+
if (!t) {
|
|
119
|
+
P("Must contain at least one letter or number");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
let n = (await q({ variables: { input: {
|
|
123
|
+
key: t,
|
|
124
|
+
label: e,
|
|
125
|
+
vocabularyId: await Z()
|
|
126
|
+
} } })).data?.createTag;
|
|
127
|
+
if (!n) throw Error("No data returned");
|
|
128
|
+
await Q(n);
|
|
129
|
+
} catch (e) {
|
|
130
|
+
P(e instanceof Error ? e.message : "Failed to create tag");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}, [
|
|
134
|
+
O,
|
|
135
|
+
j,
|
|
136
|
+
J,
|
|
137
|
+
Z,
|
|
138
|
+
q,
|
|
139
|
+
Q
|
|
140
|
+
]), pe = e(async (e) => {
|
|
141
|
+
await D(e);
|
|
142
|
+
}, [D]);
|
|
143
|
+
if (V) return null;
|
|
144
|
+
let $ = !!(O && j.trim() && !G && K.length === 0);
|
|
145
|
+
return /* @__PURE__ */ c("div", {
|
|
146
|
+
className: g("flex flex-wrap items-center gap-1.5", ce),
|
|
147
|
+
children: [T.map((e) => /* @__PURE__ */ s(ae, {
|
|
148
|
+
id: e.id,
|
|
149
|
+
label: e.label,
|
|
150
|
+
color: e.color ?? void 0,
|
|
151
|
+
icon: e.icon ?? void 0,
|
|
152
|
+
description: e.description ?? void 0,
|
|
153
|
+
removable: !0,
|
|
154
|
+
onRemove: () => void pe(e.id),
|
|
155
|
+
size: "sm"
|
|
156
|
+
}, e.id)), U && /* @__PURE__ */ c(h, {
|
|
157
|
+
open: le,
|
|
158
|
+
onOpenChange: A,
|
|
159
|
+
children: [/* @__PURE__ */ c(re, {
|
|
160
|
+
type: "button",
|
|
161
|
+
"aria-label": "Add tag",
|
|
162
|
+
className: g("inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed", "border-border text-muted-foreground text-xs", "hover:border-foreground hover:text-foreground transition-colors", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"),
|
|
163
|
+
children: [/* @__PURE__ */ s(te, { className: "size-3" }), /* @__PURE__ */ s("span", { children: T.length > 0 ? "Add" : "Add tags" })]
|
|
164
|
+
}), /* @__PURE__ */ s(ne, {
|
|
165
|
+
className: "w-64 p-0",
|
|
166
|
+
align: "start",
|
|
167
|
+
side: "bottom",
|
|
168
|
+
sideOffset: 4,
|
|
169
|
+
children: /* @__PURE__ */ c(l, {
|
|
170
|
+
shouldFilter: !1,
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ c("div", {
|
|
173
|
+
className: "flex items-center border-b border-border px-3",
|
|
174
|
+
children: [
|
|
175
|
+
/* @__PURE__ */ s(ee, { className: "size-4 shrink-0 text-muted-foreground" }),
|
|
176
|
+
/* @__PURE__ */ s(f, {
|
|
177
|
+
placeholder: "Search or create a tag...",
|
|
178
|
+
value: j,
|
|
179
|
+
onValueChange: de,
|
|
180
|
+
className: "h-9 flex-1 border-0 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground"
|
|
181
|
+
}),
|
|
182
|
+
G && /* @__PURE__ */ s(i, { className: "size-3.5 shrink-0 animate-spin text-muted-foreground" })
|
|
183
|
+
]
|
|
184
|
+
}),
|
|
185
|
+
/* @__PURE__ */ c(m, {
|
|
186
|
+
className: "max-h-48 overflow-y-auto",
|
|
187
|
+
children: [
|
|
188
|
+
j.trim() === "" && /* @__PURE__ */ s(u, {
|
|
189
|
+
className: "py-4 text-center text-xs text-muted-foreground",
|
|
190
|
+
children: "Type to search or create a tag"
|
|
191
|
+
}),
|
|
192
|
+
K.length > 0 && /* @__PURE__ */ s(d, { children: K.map((e) => /* @__PURE__ */ c(p, {
|
|
193
|
+
value: e.id,
|
|
194
|
+
onSelect: () => void Q(e),
|
|
195
|
+
className: "flex items-center gap-2 px-3 py-2 cursor-pointer",
|
|
196
|
+
disabled: F,
|
|
197
|
+
children: [
|
|
198
|
+
e.icon && /* @__PURE__ */ s("span", {
|
|
199
|
+
className: "text-sm",
|
|
200
|
+
children: e.icon
|
|
201
|
+
}),
|
|
202
|
+
/* @__PURE__ */ s("span", {
|
|
203
|
+
className: "truncate text-sm",
|
|
204
|
+
style: e.color ? { color: e.color } : void 0,
|
|
205
|
+
children: e.label
|
|
206
|
+
}),
|
|
207
|
+
F && /* @__PURE__ */ s(i, { className: "ml-auto size-3 animate-spin" })
|
|
208
|
+
]
|
|
209
|
+
}, e.id)) }),
|
|
210
|
+
$ && /* @__PURE__ */ s(d, { children: /* @__PURE__ */ c(p, {
|
|
211
|
+
value: `__create__${j}`,
|
|
212
|
+
onSelect: () => void fe(),
|
|
213
|
+
className: "flex items-center gap-2 px-3 py-2 cursor-pointer text-primary",
|
|
214
|
+
disabled: J,
|
|
215
|
+
children: [J ? /* @__PURE__ */ s(i, { className: "size-3.5 animate-spin" }) : /* @__PURE__ */ s(a, { className: "size-3.5" }), /* @__PURE__ */ s("span", {
|
|
216
|
+
className: "text-sm truncate",
|
|
217
|
+
children: J ? "Creating..." : `Create "${j}"`
|
|
218
|
+
})]
|
|
219
|
+
}) }),
|
|
220
|
+
j.trim() !== "" && !G && K.length === 0 && !$ && /* @__PURE__ */ s(u, {
|
|
221
|
+
className: "py-4 text-center text-xs text-muted-foreground",
|
|
222
|
+
children: "No tags found"
|
|
223
|
+
}),
|
|
224
|
+
N && /* @__PURE__ */ s("div", {
|
|
225
|
+
className: "px-3 py-2 text-xs text-destructive border-t border-border",
|
|
226
|
+
children: N
|
|
227
|
+
})
|
|
228
|
+
]
|
|
229
|
+
}),
|
|
230
|
+
!U && /* @__PURE__ */ c("div", {
|
|
231
|
+
className: "border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1",
|
|
232
|
+
children: [
|
|
233
|
+
/* @__PURE__ */ s(o, { className: "size-3" }),
|
|
234
|
+
"Maximum ",
|
|
235
|
+
k,
|
|
236
|
+
" tags reached"
|
|
237
|
+
]
|
|
238
|
+
})
|
|
239
|
+
]
|
|
240
|
+
})
|
|
241
|
+
})]
|
|
242
|
+
})]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
export { C as ProjectTagsWidget };
|
|
247
|
+
|
|
248
|
+
//# sourceMappingURL=ProjectTagsWidget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectTagsWidget.js","names":[],"sources":["../../../src/components/tags/ProjectTagsWidget.tsx"],"sourcesContent":["/**\n * ProjectTagsWidget\n *\n * Local replacement for `@burdenoff/fe-libs/tag` `TagsWidget`, used until\n * fe-libs ships a fix for the non-clickable `AddTagTrigger` (the published\n * trigger swallows the props Radix Slot tries to forward via\n * `<PopoverTrigger asChild>`, so the popover never opens).\n *\n * This component bypasses the broken trigger by rendering a regular\n * `<PopoverTrigger>` (no `asChild`) so Radix mounts the click-handling\n * button itself. Internals (search, create, assign, remove) mirror the\n * fe-libs `TagsWidget` so the data path through wspace-tags-svc is\n * identical.\n *\n * TODO(fe-libs): once `@burdenoff/fe-libs/tag` `AddTagTrigger` forwards\n * props/ref to the underlying button, delete this file and switch back\n * to `<TagsWidget>` from `@burdenoff/fe-libs/tag`.\n */\n\nimport { gql } from '@apollo/client';\nimport { useLazyQuery, useMutation } from '@apollo/client/react';\nimport { Loader2, Plus, Search, Tag as TagIcon, X } from 'lucide-react';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type TagItem } from '@burdenoff/fe-libs/tag';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useGatewayClient } from '@burdenoff/fe-libs/shared/graphql';\nimport { cn } from '@burdenoff/fe-libs/shared/utils';\n\n// --- GraphQL ----------------------------------------------------------------\n\ninterface VocabResult {\n id: string;\n key: string;\n name: string;\n}\n\nconst SEARCH_TAGS = gql`\n query ProjectTagsWidget_SearchTags($query: String!, $vocabularyId: ID, $limit: Int) {\n searchTags(query: $query, vocabularyId: $vocabularyId, limit: $limit) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n description\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst GET_VOCABULARY_BY_KEY = gql`\n query ProjectTagsWidget_GetVocabularyByKey($key: String!) {\n getVocabularyByKey(key: $key) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_VOCABULARY = gql`\n mutation ProjectTagsWidget_CreateVocabulary($input: CreateVocabularyInput!) {\n createVocabulary(input: $input) {\n id\n key\n name\n }\n }\n`;\n\nconst CREATE_TAG = gql`\n mutation ProjectTagsWidget_CreateTag($input: CreateTagInput!) {\n createTag(input: $input) {\n id\n key\n label\n value\n vocabularyId\n parentId\n hierarchyPath\n color\n icon\n vocabulary {\n key\n }\n }\n }\n`;\n\nconst DEFAULT_VOCAB_KEY = 'general';\n\n// --- Props ------------------------------------------------------------------\n\nexport interface ProjectTagsWidgetProps {\n /** Entity type recorded on the assigned-tag projection (e.g. \"Project\"). */\n entityType: string;\n /** Entity id (project id). */\n entityId: string;\n /** Workspace scope — widget renders nothing when empty. */\n workspaceId?: string;\n /** Controlled tag list owned by the parent. */\n tags: TagItem[];\n /** Persist a newly assigned tag on the entity. */\n onAdd: (tag: TagItem) => void | Promise<void>;\n /** Persist a tag removal on the entity. */\n onRemove: (tagId: string) => void | Promise<void>;\n /** Allow creating new tags on the fly. Default: true. */\n allowCreate?: boolean;\n /** Cap on assigned tags. Default 20. */\n maxTags?: number;\n className?: string;\n}\n\n// --- Component --------------------------------------------------------------\n\nexport function ProjectTagsWidget({\n entityType: _entityType,\n entityId,\n workspaceId,\n tags,\n onAdd,\n onRemove,\n allowCreate = true,\n maxTags = 20,\n className,\n}: ProjectTagsWidgetProps) {\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [createError, setCreateError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [resolvedVocabId, setResolvedVocabId] = useState<string | undefined>();\n const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const wsClient = useGatewayClient('workspace');\n\n const skip = !workspaceId || !entityId;\n\n const assignedTagIds = new Set(tags.map((t) => t.id));\n const canAddMore = tags.length < maxTags;\n\n const [runSearch, { data: searchData, loading: searching }] = useLazyQuery<{\n searchTags: TagItem[];\n }>(SEARCH_TAGS, { client: wsClient });\n\n const searchResults = (searchData?.searchTags ?? []).filter(\n (t: TagItem) => !assignedTagIds.has(t.id)\n );\n\n const [createTag, { loading: creating }] = useMutation<{\n createTag: TagItem;\n }>(CREATE_TAG, { client: wsClient });\n\n const [fetchVocabByKey] = useLazyQuery<{\n getVocabularyByKey: VocabResult | null;\n }>(GET_VOCABULARY_BY_KEY, { client: wsClient });\n\n const [createVocabulary] = useMutation<{\n createVocabulary: VocabResult;\n }>(CREATE_VOCABULARY, { client: wsClient });\n\n const resolveVocabId = useCallback(async (): Promise<string> => {\n if (resolvedVocabId) return resolvedVocabId;\n\n const res = await fetchVocabByKey({ variables: { key: DEFAULT_VOCAB_KEY } });\n const existing = res.data?.getVocabularyByKey;\n if (existing) {\n setResolvedVocabId(existing.id);\n return existing.id;\n }\n\n const created = await createVocabulary({\n variables: {\n input: {\n key: DEFAULT_VOCAB_KEY,\n name: 'General',\n description: 'Default vocabulary for general-purpose tags',\n scope: 'WORKSPACE',\n allowCustomTags: true,\n },\n },\n });\n const newVocab = created.data?.createVocabulary;\n if (!newVocab) throw new Error('Failed to create default vocabulary');\n setResolvedVocabId(newVocab.id);\n return newVocab.id;\n }, [resolvedVocabId, fetchVocabByKey, createVocabulary]);\n\n const handleSearchChange = useCallback(\n (value: string) => {\n setSearch(value);\n if (createError) setCreateError(null);\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (!value.trim()) return;\n debounceRef.current = setTimeout(() => {\n void runSearch({\n variables: { query: value, vocabularyId: resolvedVocabId, limit: 10 },\n });\n }, 250);\n },\n [runSearch, resolvedVocabId, createError]\n );\n\n useEffect(() => {\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, []);\n\n const handleAssign = useCallback(\n async (tag: TagItem) => {\n if (!canAddMore || adding) return;\n setAdding(true);\n try {\n const normalizedTag: TagItem = {\n ...tag,\n vocabularyKey: tag.vocabularyKey ?? tag.vocabulary?.key ?? null,\n };\n await onAdd(normalizedTag);\n setSearch('');\n setOpen(false);\n } finally {\n setAdding(false);\n }\n },\n [onAdd, canAddMore, adding]\n );\n\n const handleCreate = useCallback(async () => {\n if (!allowCreate || !search.trim() || creating) return;\n setCreateError(null);\n try {\n const label = search.trim();\n const key = label\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n if (!key) {\n setCreateError('Must contain at least one letter or number');\n return;\n }\n const vocabId = await resolveVocabId();\n const res = await createTag({\n variables: { input: { key, label, vocabularyId: vocabId } },\n });\n const created = res.data?.createTag;\n if (!created) throw new Error('No data returned');\n await handleAssign(created);\n } catch (err) {\n setCreateError(err instanceof Error ? err.message : 'Failed to create tag');\n }\n }, [allowCreate, search, creating, resolveVocabId, createTag, handleAssign]);\n\n const handleRemove = useCallback(\n async (tagId: string) => {\n await onRemove(tagId);\n },\n [onRemove]\n );\n\n if (skip) return null;\n\n const showCreateOption = Boolean(\n allowCreate && search.trim() && !searching && searchResults.length === 0\n );\n\n return (\n <div className={cn('flex flex-wrap items-center gap-1.5', className)}>\n {tags.map((tag) => (\n <TagBadge\n key={tag.id}\n id={tag.id}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => void handleRemove(tag.id)}\n size=\"sm\"\n />\n ))}\n\n {canAddMore && (\n <Popover open={open} onOpenChange={setOpen}>\n {/*\n Intentionally do NOT use `asChild` here — the fe-libs\n `AddTagTrigger` swallows the props Radix forwards, leaving the\n trigger non-clickable. Letting Radix render its own button\n restores the click handler.\n */}\n <PopoverTrigger\n type=\"button\"\n aria-label=\"Add tag\"\n className={cn(\n 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full border border-dashed',\n 'border-border text-muted-foreground text-xs',\n 'hover:border-foreground hover:text-foreground transition-colors',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'\n )}\n >\n <TagIcon className=\"size-3\" />\n <span>{tags.length > 0 ? 'Add' : 'Add tags'}</span>\n </PopoverTrigger>\n\n <PopoverContent className=\"w-64 p-0\" align=\"start\" side=\"bottom\" sideOffset={4}>\n <Command shouldFilter={false}>\n <div className=\"flex items-center border-b border-border px-3\">\n <Search className=\"size-4 shrink-0 text-muted-foreground\" />\n <CommandInput\n placeholder=\"Search or create a tag...\"\n value={search}\n onValueChange={handleSearchChange}\n className=\"h-9 flex-1 border-0 bg-transparent px-2 text-sm outline-none placeholder:text-muted-foreground\"\n />\n {searching && (\n <Loader2 className=\"size-3.5 shrink-0 animate-spin text-muted-foreground\" />\n )}\n </div>\n\n <CommandList className=\"max-h-48 overflow-y-auto\">\n {search.trim() === '' && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n Type to search or create a tag\n </CommandEmpty>\n )}\n\n {searchResults.length > 0 && (\n <CommandGroup>\n {searchResults.map((tag: TagItem) => (\n <CommandItem\n key={tag.id}\n value={tag.id}\n onSelect={() => void handleAssign(tag)}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer\"\n disabled={adding}\n >\n {tag.icon && <span className=\"text-sm\">{tag.icon}</span>}\n <span\n className=\"truncate text-sm\"\n style={tag.color ? { color: tag.color } : undefined}\n >\n {tag.label}\n </span>\n {adding && <Loader2 className=\"ml-auto size-3 animate-spin\" />}\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n {showCreateOption && (\n <CommandGroup>\n <CommandItem\n value={`__create__${search}`}\n onSelect={() => void handleCreate()}\n className=\"flex items-center gap-2 px-3 py-2 cursor-pointer text-primary\"\n disabled={creating}\n >\n {creating ? (\n <Loader2 className=\"size-3.5 animate-spin\" />\n ) : (\n <Plus className=\"size-3.5\" />\n )}\n <span className=\"text-sm truncate\">\n {creating ? 'Creating...' : `Create \"${search}\"`}\n </span>\n </CommandItem>\n </CommandGroup>\n )}\n\n {search.trim() !== '' &&\n !searching &&\n searchResults.length === 0 &&\n !showCreateOption && (\n <CommandEmpty className=\"py-4 text-center text-xs text-muted-foreground\">\n No tags found\n </CommandEmpty>\n )}\n\n {createError && (\n <div className=\"px-3 py-2 text-xs text-destructive border-t border-border\">\n {createError}\n </div>\n )}\n </CommandList>\n\n {!canAddMore && (\n <div className=\"border-t border-border px-3 py-2 text-xs text-muted-foreground flex items-center gap-1\">\n <X className=\"size-3\" />\n Maximum {maxTags} tags reached\n </div>\n )}\n </Command>\n </PopoverContent>\n </Popover>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AAgDA,IAAM,KAAc,CAAG;;;;;;;;;;;;;;;;;;GAoBjB,IAAwB,CAAG;;;;;;;;GAU3B,IAAoB,CAAG;;;;;;;;GAUvB,KAAa,CAAG;;;;;;;;;;;;;;;;;GAmBhB,IAAoB;AA0B1B,SAAgB,EAAkB,EAChC,YAAY,GACZ,aACA,gBACA,SACA,UACA,aACA,iBAAc,IACd,aAAU,IACV,iBACyB;CACzB,IAAM,CAAC,IAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAQ,KAAa,EAAS,GAAM,EACrC,CAAC,GAAiB,KAAsB,GAA8B,EACtE,IAAc,EAA6C,KAAK,EAEhE,IAAW,GAAiB,YAAY,EAExC,IAAO,CAAC,KAAe,CAAC,GAExB,IAAiB,IAAI,IAAI,EAAK,KAAK,MAAM,EAAE,GAAG,CAAC,EAC/C,IAAa,EAAK,SAAS,GAE3B,CAAC,GAAW,EAAE,MAAM,IAAY,SAAS,OAAe,EAE3D,IAAa,EAAE,QAAQ,GAAU,CAAC,EAE/B,KAAiB,IAAY,cAAc,EAAE,EAAE,QAClD,MAAe,CAAC,EAAe,IAAI,EAAE,GAAG,CAC1C,EAEK,CAAC,GAAW,EAAE,SAAS,OAAc,EAExC,IAAY,EAAE,QAAQ,GAAU,CAAC,EAE9B,CAAC,KAAmB,EAEvB,GAAuB,EAAE,QAAQ,GAAU,CAAC,EAEzC,CAAC,KAAoB,EAExB,GAAmB,EAAE,QAAQ,GAAU,CAAC,EAErC,IAAiB,EAAY,YAA6B;AAC9D,MAAI,EAAiB,QAAO;EAG5B,IAAM,KADM,MAAM,EAAgB,EAAE,WAAW,EAAE,KAAK,GAAmB,EAAE,CAAC,EACvD,MAAM;AAC3B,MAAI,EAEF,QADA,EAAmB,EAAS,GAAG,EACxB,EAAS;EAclB,IAAM,KAXU,MAAM,EAAiB,EACrC,WAAW,EACT,OAAO;GACL,KAAK;GACL,MAAM;GACN,aAAa;GACb,OAAO;GACP,iBAAiB;GAClB,EACF,EACF,CAAC,EACuB,MAAM;AAC/B,MAAI,CAAC,EAAU,OAAU,MAAM,sCAAsC;AAErE,SADA,EAAmB,EAAS,GAAG,EACxB,EAAS;IACf;EAAC;EAAiB;EAAiB;EAAiB,CAAC,EAElD,KAAqB,GACxB,MAAkB;AACjB,IAAU,EAAM,EACZ,KAAa,EAAe,KAAK,EACjC,EAAY,WAAS,aAAa,EAAY,QAAQ,EACrD,EAAM,MAAM,KACjB,EAAY,UAAU,iBAAiB;AAChC,KAAU,EACb,WAAW;IAAE,OAAO;IAAO,cAAc;IAAiB,OAAO;IAAI,EACtE,CAAC;KACD,IAAI;IAET;EAAC;EAAW;EAAiB;EAAY,CAC1C;AAED,eACe;AACX,EAAI,EAAY,WAAS,aAAa,EAAY,QAAQ;IAE3D,EAAE,CAAC;CAEN,IAAM,IAAe,EACnB,OAAO,MAAiB;AAClB,SAAC,KAAc,IACnB;KAAU,GAAK;AACf,OAAI;AAOF,IAFA,MAAM,EAJyB;KAC7B,GAAG;KACH,eAAe,EAAI,iBAAiB,EAAI,YAAY,OAAO;KAC5D,CACyB,EAC1B,EAAU,GAAG,EACb,EAAQ,GAAM;aACN;AACR,MAAU,GAAM;;;IAGpB;EAAC;EAAO;EAAY;EAAO,CAC5B,EAEK,KAAe,EAAY,YAAY;AACvC,SAAC,KAAe,CAAC,EAAO,MAAM,IAAI,IACtC;KAAe,KAAK;AACpB,OAAI;IACF,IAAM,IAAQ,EAAO,MAAM,EACrB,IAAM,EACT,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,UAAU,GAAG;AACxB,QAAI,CAAC,GAAK;AACR,OAAe,6CAA6C;AAC5D;;IAMF,IAAM,KAHM,MAAM,EAAU,EAC1B,WAAW,EAAE,OAAO;KAAE;KAAK;KAAO,cAFpB,MAAM,GAAgB;KAEqB,EAAE,EAC5D,CAAC,EACkB,MAAM;AAC1B,QAAI,CAAC,EAAS,OAAU,MAAM,mBAAmB;AACjD,UAAM,EAAa,EAAQ;YACpB,GAAK;AACZ,MAAe,aAAe,QAAQ,EAAI,UAAU,uBAAuB;;;IAE5E;EAAC;EAAa;EAAQ;EAAU;EAAgB;EAAW;EAAa,CAAC,EAEtE,KAAe,EACnB,OAAO,MAAkB;AACvB,QAAM,EAAS,EAAM;IAEvB,CAAC,EAAS,CACX;AAED,KAAI,EAAM,QAAO;CAEjB,IAAM,IAAmB,GACvB,KAAe,EAAO,MAAM,IAAI,CAAC,KAAa,EAAc,WAAW;AAGzE,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uCAAuC,GAAU;YAApE,CACG,EAAK,KAAK,MACT,kBAAC,IAAD;GAEE,IAAI,EAAI;GACR,OAAO,EAAI;GACX,OAAO,EAAI,SAAS,KAAA;GACpB,MAAM,EAAI,QAAQ,KAAA;GAClB,aAAa,EAAI,eAAe,KAAA;GAChC,WAAA;GACA,gBAAgB,KAAK,GAAa,EAAI,GAAG;GACzC,MAAK;GACL,EATK,EAAI,GAST,CACF,EAED,KACC,kBAAC,GAAD;GAAe;GAAM,cAAc;aAAnC,CAOE,kBAAC,IAAD;IACE,MAAK;IACL,cAAW;IACX,WAAW,EACT,gFACA,+CACA,mEACA,0EACD;cARH,CAUE,kBAAC,IAAD,EAAS,WAAU,UAAW,CAAA,EAC9B,kBAAC,QAAD,EAAA,UAAO,EAAK,SAAS,IAAI,QAAQ,YAAkB,CAAA,CACpC;OAEjB,kBAAC,IAAD;IAAgB,WAAU;IAAW,OAAM;IAAQ,MAAK;IAAS,YAAY;cAC3E,kBAAC,GAAD;KAAS,cAAc;eAAvB;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,IAAD,EAAQ,WAAU,yCAA0C,CAAA;QAC5D,kBAAC,GAAD;SACE,aAAY;SACZ,OAAO;SACP,eAAe;SACf,WAAU;SACV,CAAA;QACD,KACC,kBAAC,GAAD,EAAS,WAAU,wDAAyD,CAAA;QAE1E;;MAEN,kBAAC,GAAD;OAAa,WAAU;iBAAvB;QACG,EAAO,MAAM,KAAK,MACjB,kBAAC,GAAD;SAAc,WAAU;mBAAiD;SAE1D,CAAA;QAGhB,EAAc,SAAS,KACtB,kBAAC,GAAD,EAAA,UACG,EAAc,KAAK,MAClB,kBAAC,GAAD;SAEE,OAAO,EAAI;SACX,gBAAgB,KAAK,EAAa,EAAI;SACtC,WAAU;SACV,UAAU;mBALZ;UAOG,EAAI,QAAQ,kBAAC,QAAD;WAAM,WAAU;qBAAW,EAAI;WAAY,CAAA;UACxD,kBAAC,QAAD;WACE,WAAU;WACV,OAAO,EAAI,QAAQ,EAAE,OAAO,EAAI,OAAO,GAAG,KAAA;qBAEzC,EAAI;WACA,CAAA;UACN,KAAU,kBAAC,GAAD,EAAS,WAAU,+BAAgC,CAAA;UAClD;WAdP,EAAI,GAcG,CACd,EACW,CAAA;QAGhB,KACC,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;SACE,OAAO,aAAa;SACpB,gBAAgB,KAAK,IAAc;SACnC,WAAU;SACV,UAAU;mBAJZ,CAMG,IACC,kBAAC,GAAD,EAAS,WAAU,yBAA0B,CAAA,GAE7C,kBAAC,GAAD,EAAM,WAAU,YAAa,CAAA,EAE/B,kBAAC,QAAD;UAAM,WAAU;oBACb,IAAW,gBAAgB,WAAW,EAAO;UACzC,CAAA,CACK;YACD,CAAA;QAGhB,EAAO,MAAM,KAAK,MACjB,CAAC,KACD,EAAc,WAAW,KACzB,CAAC,KACC,kBAAC,GAAD;SAAc,WAAU;mBAAiD;SAE1D,CAAA;QAGlB,KACC,kBAAC,OAAD;SAAK,WAAU;mBACZ;SACG,CAAA;QAEI;;MAEb,CAAC,KACA,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAG,WAAU,UAAW,CAAA;;QACf;QAAQ;QACb;;MAEA;;IACK,CAAA,CACT;KAER"}
|
|
@@ -6,7 +6,7 @@ import { graphqlFetch as i } from "@burdenoff/fe-libs/shared/graphql";
|
|
|
6
6
|
//#region src/hooks/useOrgsWithPermission.ts
|
|
7
7
|
var a = 50, o = "\n query CheckRbacPermissions($inputs: [PermissionCheckInput!]!) {\n checkPermissions(inputs: $inputs) {\n allowed\n }\n }\n";
|
|
8
8
|
function s(s, c, l) {
|
|
9
|
-
let { user: u } = n(), { authToken: d } = e(), f = u?.id ?? "", p = !!(f && s.length > 0), m = t(() => s.join(","), [s]), h = t(() => [
|
|
9
|
+
let { user: u } = n(), { authToken: d } = e(), f = u?.id ?? "", p = !!(f && s.length > 0), m = t(() => s.join(","), [s]), h = t(() => m ? m.split(",") : [], [m]), { data: g, isLoading: _, error: v } = r({
|
|
10
10
|
queryKey: [
|
|
11
11
|
"checkPermissions",
|
|
12
12
|
f,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOrgsWithPermission.js","names":[],"sources":["../../src/hooks/useOrgsWithPermission.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\n\n/**\n * Filter a set of organization ids down to those where the current user has\n * a specific RBAC permission. Bulk-checks against the rbac service so the\n * result tracks the authoritative permission evaluator — including\n * overrides and future OPA policies — instead of duplicating role→permission\n * logic on the client.\n *\n * The server caps a single bulk call at `CHECK_PERMISSIONS_BATCH_LIMIT`\n * inputs, so when a user has more orgs than that we chunk into multiple\n * parallel calls and merge the results positionally. Most users will fit\n * in one batch; the chunking is just defensive for power users / service\n * accounts with many memberships.\n *\n * Backend `@rbac` directives still enforce on the mutation; this filter is\n * purely UX so users aren't offered options that will 403 on submit.\n *\n * Note: this MFE talks to the gateway via the shared `graphqlFetch` helper\n * + react-query (not Apollo), so we mirror that pattern instead of using\n * the codegen'd Apollo hook. The duplicate `.graphql` file at\n * `src/operations/global/rbac/check-permissions.graphql` is intentionally\n * kept so when this MFE eventually migrates to the codegen'd pattern (see\n * todo.md, \"Issue to address later\"), the operation already exists.\n */\n\n// Matches the server-side limit on `checkPermissions(inputs: [...])` —\n// kept here as a constant so the chunking intent is explicit.\nconst CHECK_PERMISSIONS_BATCH_LIMIT = 50;\n\n// `decisionPath` is intentionally NOT selected — only `allowed` drives\n// the UI here, and `decisionPath` exposes RBAC policy internals (role\n// names, override decisions) that we don't want to leak into network\n// traffic visible in browser devtools just for UX filtering.\nconst CHECK_PERMISSIONS_QUERY = `\n query CheckRbacPermissions($inputs: [PermissionCheckInput!]!) {\n checkPermissions(inputs: $inputs) {\n allowed\n }\n }\n`;\n\ninterface PermissionCheckResult {\n allowed: boolean;\n}\n\nexport interface UseOrgsWithPermissionResult {\n allowedIds: Set<string>;\n loading: boolean;\n isError: boolean;\n}\n\nexport function useOrgsWithPermission(\n orgIds: readonly string[],\n resourceType: string,\n action: string\n): UseOrgsWithPermissionResult {\n const { user } = useAuth();\n const { authToken } = useWorkspacesContext();\n const actorId = user?.id ?? '';\n\n const enabled = Boolean(actorId && orgIds.length > 0);\n\n // `stableIds` only needs to change when the *values* change — a fresh array\n // with identical contents shouldn't bust the query cache or trigger refetch.\n // Use the joined string as the memo key so deep-equal arrays share a memo.\n // Separator MUST be a char that can't appear in an id (',') — joining\n // on '' would collide on adjacency: ['ab','cd'] and ['a','bcd'] would\n // share a cache key.\n const stableKey = useMemo(() => orgIds.join(','), [orgIds]);\n const stableIds = useMemo(() => [
|
|
1
|
+
{"version":3,"file":"useOrgsWithPermission.js","names":[],"sources":["../../src/hooks/useOrgsWithPermission.ts"],"sourcesContent":["import { useMemo } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\n\n/**\n * Filter a set of organization ids down to those where the current user has\n * a specific RBAC permission. Bulk-checks against the rbac service so the\n * result tracks the authoritative permission evaluator — including\n * overrides and future OPA policies — instead of duplicating role→permission\n * logic on the client.\n *\n * The server caps a single bulk call at `CHECK_PERMISSIONS_BATCH_LIMIT`\n * inputs, so when a user has more orgs than that we chunk into multiple\n * parallel calls and merge the results positionally. Most users will fit\n * in one batch; the chunking is just defensive for power users / service\n * accounts with many memberships.\n *\n * Backend `@rbac` directives still enforce on the mutation; this filter is\n * purely UX so users aren't offered options that will 403 on submit.\n *\n * Note: this MFE talks to the gateway via the shared `graphqlFetch` helper\n * + react-query (not Apollo), so we mirror that pattern instead of using\n * the codegen'd Apollo hook. The duplicate `.graphql` file at\n * `src/operations/global/rbac/check-permissions.graphql` is intentionally\n * kept so when this MFE eventually migrates to the codegen'd pattern (see\n * todo.md, \"Issue to address later\"), the operation already exists.\n */\n\n// Matches the server-side limit on `checkPermissions(inputs: [...])` —\n// kept here as a constant so the chunking intent is explicit.\nconst CHECK_PERMISSIONS_BATCH_LIMIT = 50;\n\n// `decisionPath` is intentionally NOT selected — only `allowed` drives\n// the UI here, and `decisionPath` exposes RBAC policy internals (role\n// names, override decisions) that we don't want to leak into network\n// traffic visible in browser devtools just for UX filtering.\nconst CHECK_PERMISSIONS_QUERY = `\n query CheckRbacPermissions($inputs: [PermissionCheckInput!]!) {\n checkPermissions(inputs: $inputs) {\n allowed\n }\n }\n`;\n\ninterface PermissionCheckResult {\n allowed: boolean;\n}\n\nexport interface UseOrgsWithPermissionResult {\n allowedIds: Set<string>;\n loading: boolean;\n isError: boolean;\n}\n\nexport function useOrgsWithPermission(\n orgIds: readonly string[],\n resourceType: string,\n action: string\n): UseOrgsWithPermissionResult {\n const { user } = useAuth();\n const { authToken } = useWorkspacesContext();\n const actorId = user?.id ?? '';\n\n const enabled = Boolean(actorId && orgIds.length > 0);\n\n // `stableIds` only needs to change when the *values* change — a fresh array\n // with identical contents shouldn't bust the query cache or trigger refetch.\n // Use the joined string as the memo key so deep-equal arrays share a memo.\n // Separator MUST be a char that can't appear in an id (',') — joining\n // on '' would collide on adjacency: ['ab','cd'] and ['a','bcd'] would\n // share a cache key.\n const stableKey = useMemo(() => orgIds.join(','), [orgIds]);\n const stableIds = useMemo(() => (stableKey ? stableKey.split(',') : []), [stableKey]);\n\n const { data, isLoading, error } = useQuery({\n queryKey: ['checkPermissions', actorId, resourceType, action, stableKey],\n enabled,\n queryFn: async (): Promise<PermissionCheckResult[]> => {\n // Chunk into <=CHECK_PERMISSIONS_BATCH_LIMIT-sized batches so users\n // with more orgs than the server cap still get a complete answer.\n // Run batches in parallel and concat in input order so the\n // positional contract relied on below holds.\n const chunks: string[][] = [];\n for (let i = 0; i < stableIds.length; i += CHECK_PERMISSIONS_BATCH_LIMIT) {\n chunks.push(stableIds.slice(i, i + CHECK_PERMISSIONS_BATCH_LIMIT));\n }\n\n const batchResults = await Promise.all(\n chunks.map(async (chunk) => {\n const result = await graphqlFetch<{ checkPermissions: PermissionCheckResult[] }>({\n gateway: 'global',\n authToken: authToken || undefined,\n query: CHECK_PERMISSIONS_QUERY,\n variables: {\n inputs: chunk.map((scopeId) => ({\n actorId,\n actorType: 'USER',\n scopeId,\n resourceType,\n action,\n })),\n },\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message || 'Failed to check permissions');\n }\n return result.data?.checkPermissions ?? [];\n })\n );\n\n return batchResults.flat();\n },\n staleTime: 60 * 1000,\n });\n\n const allowedIds = useMemo(() => {\n const out = new Set<string>();\n if (!data) return out;\n // Positional mapping is the documented contract of the bulk\n // `checkPermissions` API — results come back in the same order as\n // inputs. Length-guard against a backend that ever reorders/filters\n // so we never silently mis-attribute a permission to the wrong org.\n const bounded = Math.min(data.length, stableIds.length);\n for (let i = 0; i < bounded; i += 1) {\n if (data[i].allowed && stableIds[i]) out.add(stableIds[i]);\n }\n return out;\n }, [data, stableIds]);\n\n return {\n allowedIds,\n loading: enabled && isLoading,\n // Surface error so callers can decide between \"stay in loading state\"\n // and \"show an error\". Default behavior should be the former — the\n // backend @rbac directive still enforces correctly on submit, so it's\n // safer to keep the form visible than to lock the user out on a\n // transient rbac-svc blip.\n isError: enabled && Boolean(error),\n };\n}\n"],"mappings":";;;;;;AAgCA,IAAM,IAAgC,IAMhC,IAA0B;AAkBhC,SAAgB,EACd,GACA,GACA,GAC6B;CAC7B,IAAM,EAAE,YAAS,GAAS,EACpB,EAAE,iBAAc,GAAsB,EACtC,IAAU,GAAM,MAAM,IAEtB,IAAU,GAAQ,KAAW,EAAO,SAAS,IAQ7C,IAAY,QAAc,EAAO,KAAK,IAAI,EAAE,CAAC,EAAO,CAAC,EACrD,IAAY,QAAe,IAAY,EAAU,MAAM,IAAI,GAAG,EAAE,EAAG,CAAC,EAAU,CAAC,EAE/E,EAAE,SAAM,cAAW,aAAU,EAAS;EAC1C,UAAU;GAAC;GAAoB;GAAS;GAAc;GAAQ;GAAU;EACxE;EACA,SAAS,YAA8C;GAKrD,IAAM,IAAqB,EAAE;AAC7B,QAAK,IAAI,IAAI,GAAG,IAAI,EAAU,QAAQ,KAAK,EACzC,GAAO,KAAK,EAAU,MAAM,GAAG,IAAI,EAA8B,CAAC;AA2BpE,WAxBqB,MAAM,QAAQ,IACjC,EAAO,IAAI,OAAO,MAAU;IAC1B,IAAM,IAAS,MAAM,EAA4D;KAC/E,SAAS;KACT,WAAW,KAAa,KAAA;KACxB,OAAO;KACP,WAAW,EACT,QAAQ,EAAM,KAAK,OAAa;MAC9B;MACA,WAAW;MACX;MACA;MACA;MACD,EAAE,EACJ;KACF,CAAC;AAEF,QAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,8BAA8B;AAE7E,WAAO,EAAO,MAAM,oBAAoB,EAAE;KAC1C,CACH,EAEmB,MAAM;;EAE5B,WAAW,KAAK;EACjB,CAAC;AAgBF,QAAO;EACL,YAfiB,QAAc;GAC/B,IAAM,oBAAM,IAAI,KAAa;AAC7B,OAAI,CAAC,EAAM,QAAO;GAKlB,IAAM,IAAU,KAAK,IAAI,EAAK,QAAQ,EAAU,OAAO;AACvD,QAAK,IAAI,IAAI,GAAG,IAAI,GAAS,KAAK,EAChC,CAAI,EAAK,GAAG,WAAW,EAAU,MAAI,EAAI,IAAI,EAAU,GAAG;AAE5D,UAAO;KACN,CAAC,GAAM,EAAU,CAAC;EAInB,SAAS,KAAW;EAMpB,SAAS,KAAW,EAAQ;EAC7B"}
|