@burdenoff/microfe-workspaces 2026.915.1 → 2026.1006.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 +7 -7
- package/dist/WorkspacesRoot.js.map +1 -1
- package/dist/WorkspacesRoutes.js +2 -2
- package/dist/WorkspacesRoutes.js.map +1 -1
- package/dist/constants/permissions.js +1 -1
- package/dist/constants/permissions.js.map +1 -1
- package/dist/pages/dashboard/DashboardCustomizer.js +112 -52
- package/dist/pages/dashboard/DashboardCustomizer.js.map +1 -1
- package/dist/pages/dashboard/hooks/useDashboardData.js +17 -11
- package/dist/pages/dashboard/hooks/useDashboardData.js.map +1 -1
- package/dist/pages/dashboard/reorder.js +26 -0
- package/dist/pages/dashboard/reorder.js.map +1 -0
- package/package.json +1 -1
package/dist/WorkspacesRoot.js
CHANGED
|
@@ -7,10 +7,7 @@ import { FeatureFlagProvider as l, useFeatureFlags as u } from "@burdenoff/fe-li
|
|
|
7
7
|
import { jsx as d } from "react/jsx-runtime";
|
|
8
8
|
//#region src/WorkspacesRoot.tsx
|
|
9
9
|
var f = (f) => {
|
|
10
|
-
let { registerNavItems: p, registerFooterItems: m, basePath: h = "/" } = f, { isEnabled: g, evaluations: _ } = u(), v = (e, t) => (e.endsWith("/") ? e.slice(0, -1) : e) + (t.startsWith("/") ? t : `/${t}`)
|
|
11
|
-
isEnabled: g,
|
|
12
|
-
evaluations: _
|
|
13
|
-
}), [_, g]);
|
|
10
|
+
let { registerNavItems: p, registerFooterItems: m, basePath: h = "/" } = f, { isEnabled: g, evaluations: _ } = u(), v = (e, t) => (e.endsWith("/") ? e.slice(0, -1) : e) + (t.startsWith("/") ? t : `/${t}`);
|
|
14
11
|
return r(() => {
|
|
15
12
|
if (p) return p([
|
|
16
13
|
{
|
|
@@ -37,19 +34,22 @@ var f = (f) => {
|
|
|
37
34
|
order: 30,
|
|
38
35
|
group: "workspaces"
|
|
39
36
|
},
|
|
40
|
-
|
|
37
|
+
{
|
|
41
38
|
id: "workspaces-activity",
|
|
42
39
|
label: "Activity",
|
|
43
40
|
path: v(h, "activity"),
|
|
44
41
|
icon: /* @__PURE__ */ d(a, { size: 20 }),
|
|
45
42
|
order: 40,
|
|
46
43
|
group: "workspaces"
|
|
47
|
-
}
|
|
44
|
+
}
|
|
48
45
|
]);
|
|
49
46
|
}, [
|
|
50
47
|
p,
|
|
51
48
|
h,
|
|
52
|
-
|
|
49
|
+
i(() => ({
|
|
50
|
+
isEnabled: g,
|
|
51
|
+
evaluations: _
|
|
52
|
+
}), [_, g])
|
|
53
53
|
]), r(() => {
|
|
54
54
|
if (m) return m([{
|
|
55
55
|
id: "workspaces-status",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspacesRoot.js","names":[],"sources":["../src/WorkspacesRoot.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useEffect, useMemo } from 'react';\nimport { Building2, FolderKanban, Users, Activity } from 'lucide-react';\nimport { useFeatureFlags, FeatureFlagProvider } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { WorkspacesProvider } from './providers/WorkspacesProvider';\nimport { WorkspacesErrorBoundary } from './components/WorkspacesErrorBoundary';\nimport type { NavItem, WorkspacesRootProps } from './types';\nimport { WorkspacesRoutes } from './WorkspacesRoutes';\n\n/**\n * Root component for the Workspaces microfrontend.\n *\n * This is the main entry point that host applications import and render.\n * It sets up the necessary providers and renders the internal routing.\n * Automatically registers navigation items with the shell if registration functions are provided.\n * Nav items for feature-previewed pages (projects, members, activity) are conditionally\n * hidden based on feature flags.\n *\n * @example\n * ```tsx\n * import { WorkspacesRoot } from '@burdenoff/micro-fe/workspaces';\n *\n * function App() {\n * return (\n * <Route path=\"/workspaces/*\" element={\n * <WorkspacesRoot\n * basePath=\"/workspaces\"\n * navigate={navigate}\n * currentUser={user}\n * workspaceId=\"ws-123\"\n * tenantId=\"tenant-123\"\n * apiGatewayUrl=\"https://gw-global.local.burdenoff.com\"\n * authToken=\"token\"\n * />\n * } />\n * );\n * }\n * ```\n */\nexport const WorkspacesRoot: FC<WorkspacesRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = '/' } = props;\n const { isEnabled, evaluations } = useFeatureFlags();\n\n // Helper to join paths correctly, avoiding double slashes\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n const flagHelpers = useMemo(() => ({ isEnabled, evaluations }), [evaluations, isEnabled]);\n\n // Register navigation items with the shell\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n {\n id: 'workspaces-all',\n label: 'All Workspaces',\n path: joinPath(basePath, 'list'),\n icon: <Building2 size={20} />,\n order: 10,\n group: 'workspaces',\n },\n // Projects — core feature, never gated\n {\n id: 'workspaces-projects',\n label: 'Projects',\n path: joinPath(basePath, 'projects'),\n icon: <FolderKanban size={20} />,\n order: 20,\n group: 'workspaces',\n },\n // Members — core feature, never gated\n {\n id: 'workspaces-members',\n label: 'Members',\n path: joinPath(basePath, 'members'),\n icon: <Users size={20} />,\n order: 30,\n group: 'workspaces',\n },\n //
|
|
1
|
+
{"version":3,"file":"WorkspacesRoot.js","names":[],"sources":["../src/WorkspacesRoot.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useEffect, useMemo } from 'react';\nimport { Building2, FolderKanban, Users, Activity } from 'lucide-react';\nimport { useFeatureFlags, FeatureFlagProvider } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { WorkspacesProvider } from './providers/WorkspacesProvider';\nimport { WorkspacesErrorBoundary } from './components/WorkspacesErrorBoundary';\nimport type { NavItem, WorkspacesRootProps } from './types';\nimport { WorkspacesRoutes } from './WorkspacesRoutes';\n\n/**\n * Root component for the Workspaces microfrontend.\n *\n * This is the main entry point that host applications import and render.\n * It sets up the necessary providers and renders the internal routing.\n * Automatically registers navigation items with the shell if registration functions are provided.\n * Nav items for feature-previewed pages (projects, members, activity) are conditionally\n * hidden based on feature flags.\n *\n * @example\n * ```tsx\n * import { WorkspacesRoot } from '@burdenoff/micro-fe/workspaces';\n *\n * function App() {\n * return (\n * <Route path=\"/workspaces/*\" element={\n * <WorkspacesRoot\n * basePath=\"/workspaces\"\n * navigate={navigate}\n * currentUser={user}\n * workspaceId=\"ws-123\"\n * tenantId=\"tenant-123\"\n * apiGatewayUrl=\"https://gw-global.local.burdenoff.com\"\n * authToken=\"token\"\n * />\n * } />\n * );\n * }\n * ```\n */\nexport const WorkspacesRoot: FC<WorkspacesRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = '/' } = props;\n const { isEnabled, evaluations } = useFeatureFlags();\n\n // Helper to join paths correctly, avoiding double slashes\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith('/') ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith('/') ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n const flagHelpers = useMemo(() => ({ isEnabled, evaluations }), [evaluations, isEnabled]);\n\n // Register navigation items with the shell\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n {\n id: 'workspaces-all',\n label: 'All Workspaces',\n path: joinPath(basePath, 'list'),\n icon: <Building2 size={20} />,\n order: 10,\n group: 'workspaces',\n },\n // Projects — core feature, never gated\n {\n id: 'workspaces-projects',\n label: 'Projects',\n path: joinPath(basePath, 'projects'),\n icon: <FolderKanban size={20} />,\n order: 20,\n group: 'workspaces',\n },\n // Members — core feature, never gated\n {\n id: 'workspaces-members',\n label: 'Members',\n path: joinPath(basePath, 'members'),\n icon: <Users size={20} />,\n order: 30,\n group: 'workspaces',\n },\n // Activity — core feature, always registered (BOFF-7247)\n {\n id: 'workspaces-activity',\n label: 'Activity',\n path: joinPath(basePath, 'activity'),\n icon: <Activity size={20} />,\n order: 40,\n group: 'workspaces',\n },\n ];\n\n // Register and get cleanup function\n const cleanup = registerNavItems(navItems);\n\n // Return cleanup function to unregister on unmount\n return cleanup;\n }, [registerNavItems, basePath, flagHelpers]);\n\n // Register footer items (example: workspace status in left section)\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: 'workspaces-status',\n label: 'Workspaces Ready',\n type: 'status' as const,\n order: 10,\n section: 'left' as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems]);\n\n return (\n <FeatureFlagProvider workspaceId={null} gateway=\"global\" defaultEnabled={true}>\n <WorkspacesErrorBoundary>\n <WorkspacesProvider {...props}>\n <WorkspacesRoutes />\n </WorkspacesProvider>\n </WorkspacesErrorBoundary>\n </FeatureFlagProvider>\n );\n};\n"],"mappings":";;;;;;;;AAuCA,IAAa,KAA2C,MAAU;CAChE,IAAM,EAAE,qBAAkB,wBAAqB,cAAW,QAAQ,GAC5D,EAAE,cAAW,mBAAgB,GAAiB,EAG9C,KAAY,GAAc,OACZ,EAAK,SAAS,IAAI,GAAG,EAAK,MAAM,GAAG,GAAG,GAAG,MACzC,EAAK,WAAW,IAAI,GAAG,IAAO,IAAI;AAyEtD,QAlEA,QAAgB;AACT,QA4CL,QAHgB,EAvCY;GAC1B;IACE,IAAI;IACJ,OAAO;IACP,MAAM,EAAS,GAAU,OAAO;IAChC,MAAM,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA;IAC7B,OAAO;IACP,OAAO;IACR;GAED;IACE,IAAI;IACJ,OAAO;IACP,MAAM,EAAS,GAAU,WAAW;IACpC,MAAM,kBAAC,GAAD,EAAc,MAAM,IAAM,CAAA;IAChC,OAAO;IACP,OAAO;IACR;GAED;IACE,IAAI;IACJ,OAAO;IACP,MAAM,EAAS,GAAU,UAAU;IACnC,MAAM,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;IACzB,OAAO;IACP,OAAO;IACR;GAED;IACE,IAAI;IACJ,OAAO;IACP,MAAM,EAAS,GAAU,WAAW;IACpC,MAAM,kBAAC,GAAD,EAAU,MAAM,IAAM,CAAA;IAC5B,OAAO;IACP,OAAO;IACR;GACF,CAGyC;IAIzC;EAAC;EAAkB;EAjDF,SAAe;GAAE;GAAW;GAAa,GAAG,CAAC,GAAa,EAAU,CAAC;EAiD7C,CAAC,EAG7C,QAAgB;AACT,QAaL,QADgB,EAVI,CAClB;GACE,IAAI;GACJ,OAAO;GACP,MAAM;GACN,OAAO;GACP,SAAS;GACV,CACF,CAE+C;IAE/C,CAAC,EAAoB,CAAC,EAGvB,kBAAC,GAAD;EAAqB,aAAa;EAAM,SAAQ;EAAS,gBAAgB;YACvE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;GAAoB,GAAI;aACtB,kBAAC,GAAD,EAAoB,CAAA;GACD,CAAA,EACG,CAAA;EACN,CAAA"}
|
package/dist/WorkspacesRoutes.js
CHANGED
|
@@ -116,11 +116,11 @@ function D() {
|
|
|
116
116
|
}),
|
|
117
117
|
/* @__PURE__ */ o(l, {
|
|
118
118
|
path: "activity",
|
|
119
|
-
element:
|
|
119
|
+
element: /* @__PURE__ */ o(v, {})
|
|
120
120
|
}),
|
|
121
121
|
/* @__PURE__ */ o(l, {
|
|
122
122
|
path: ":workspaceId/activity",
|
|
123
|
-
element:
|
|
123
|
+
element: /* @__PURE__ */ o(v, {})
|
|
124
124
|
}),
|
|
125
125
|
/* @__PURE__ */ o(l, {
|
|
126
126
|
path: "*",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspacesRoutes.js","names":[],"sources":["../src/WorkspacesRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate } from 'react-router';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useWorkspacesContext } from './providers/WorkspacesProvider';\nimport { WorkspaceContextPathSync } from './components/WorkspaceContextPathSync';\n\n// Eager load - main entry point\nimport { WorkspacesListPage } from './pages/WorkspacesListPage';\n\n// Lazy load - secondary pages\nconst CreateWorkspacePage = lazy(() =>\n import('./pages/CreateWorkspacePage').then((m) => ({ default: m.CreateWorkspacePage }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./pages/WorkspaceDetailPage').then((m) => ({ default: m.WorkspaceDetailPage }))\n);\nconst WorkspaceSettingsPage = lazy(() =>\n import('./pages/WorkspaceSettingsPage').then((m) => ({ default: m.WorkspaceSettingsPage }))\n);\nconst ProjectsListPage = lazy(() =>\n import('./pages/ProjectsListPage').then((m) => ({ default: m.ProjectsListPage }))\n);\nconst ProjectDetailPage = lazy(() =>\n import('./pages/ProjectDetailPage').then((m) => ({ default: m.ProjectDetailPage }))\n);\nconst ProjectMembersPage = lazy(() =>\n import('./pages/ProjectMembersPage').then((m) => ({ default: m.ProjectMembersPage }))\n);\nconst MembersListPage = lazy(() =>\n import('./pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst ActivityListPage = lazy(() =>\n import('./pages/ActivityListPage').then((m) => ({ default: m.ActivityListPage }))\n);\nconst InviteAcceptancePage = lazy(() =>\n import('./pages/InviteAcceptancePage').then((m) => ({ default: m.InviteAcceptancePage }))\n);\nconst PendingInvitesPage = lazy(() =>\n import('./pages/PendingInvitesPage').then((m) => ({ default: m.PendingInvitesPage }))\n);\nconst WorkspaceInvitePage = lazy(() =>\n import('./pages/WorkspaceInvitePage').then((m) => ({ default: m.WorkspaceInvitePage }))\n);\n\n/**\n * Loading fallback for lazy-loaded components\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"animate-spin rounded-full size-8 border-b-2 border-primary\" />\n </div>\n );\n}\n\n/**\n * Fallback shown when a feature-gated route is disabled.\n */\nfunction FeatureDisabledFallback() {\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"text-lg font-semibold mb-2 text-text-primary\">Feature Not Available</h2>\n <p className=\"text-sm text-text-secondary\">\n This feature is not currently available. Enable it under Feature Previews to access this\n functionality.\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Helper: wraps a page element in a FeatureGate.\n * defaultValue=false → hidden until explicitly enabled (unreleased feature).\n */\nfunction gated(flag: string, element: React.ReactNode, defaultValue = false): React.ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n/**\n * Catch-all redirect component that uses the basePath from context\n * to navigate to an absolute path, preventing infinite redirect loops.\n */\nfunction CatchAllRedirect() {\n const { basePath = '/workspaces' } = useWorkspacesContext();\n return <Navigate to={`${basePath}/list`} replace />;\n}\n\nfunction CurrentWorkspaceInviteRedirect() {\n const { basePath = '/workspaces', workspaceId } = useWorkspacesContext();\n\n if (!workspaceId) {\n return <WorkspaceInvitePage />;\n }\n\n return <Navigate to={`${basePath}/${workspaceId}/invite`} replace />;\n}\n\n/**\n * Internal routing for the Workspaces microfrontend.\n * Defines all routes and their corresponding page components.\n *\n * - WorkspacesListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better performance\n * - Projects, Members, Activity: gated by feature flags (unreleased)\n */\nexport function WorkspacesRoutes() {\n return (\n <Suspense fallback={<PageLoader />}>\n {/* Keeps the scoped `:workspaceId` URL segment in step with the active context\n when the user switches workspace via the chrome context switcher. */}\n <WorkspaceContextPathSync />\n <Routes>\n {/* Default redirect */}\n <Route index element={<Navigate to=\"list\" replace />} />\n\n {/* Workspaces List - Eager loaded */}\n <Route path=\"list\" element={<WorkspacesListPage />} />\n\n {/* Create Workspace (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"new\" element={<CreateWorkspacePage />} />\n\n {/* Pending Invites (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invites\" element={<PendingInvitesPage />} />\n\n {/* Workspace Invite */}\n <Route path=\"invite\" element={<CurrentWorkspaceInviteRedirect />} />\n\n {/* Invitation Acceptance (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invite/:invitationId\" element={<InviteAcceptancePage />} />\n\n {/* Workspace Detail */}\n <Route path=\":workspaceId\" element={<WorkspaceDetailPage />} />\n <Route\n path=\":workspaceId/settings\"\n element={gated('workspaces.settings', <WorkspaceSettingsPage />, true)}\n />\n <Route path=\":workspaceId/invite\" element={<WorkspaceInvitePage />} />\n\n {/* Projects — core feature, never gated */}\n <Route path=\"projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects/:projectId\" element={<ProjectDetailPage />} />\n <Route path=\":workspaceId/projects/:projectId/members\" element={<ProjectMembersPage />} />\n\n {/* Members — core feature, never gated */}\n <Route path=\"members\" element={<MembersListPage />} />\n <Route path=\":workspaceId/members\" element={<MembersListPage />} />\n\n {/* Activity — gated
|
|
1
|
+
{"version":3,"file":"WorkspacesRoutes.js","names":[],"sources":["../src/WorkspacesRoutes.tsx"],"sourcesContent":["import { lazy, Suspense } from 'react';\nimport { Routes, Route, Navigate } from 'react-router';\nimport { FeatureGate } from '@burdenoff/fe-libs/shared/feature-flags';\nimport { useWorkspacesContext } from './providers/WorkspacesProvider';\nimport { WorkspaceContextPathSync } from './components/WorkspaceContextPathSync';\n\n// Eager load - main entry point\nimport { WorkspacesListPage } from './pages/WorkspacesListPage';\n\n// Lazy load - secondary pages\nconst CreateWorkspacePage = lazy(() =>\n import('./pages/CreateWorkspacePage').then((m) => ({ default: m.CreateWorkspacePage }))\n);\nconst WorkspaceDetailPage = lazy(() =>\n import('./pages/WorkspaceDetailPage').then((m) => ({ default: m.WorkspaceDetailPage }))\n);\nconst WorkspaceSettingsPage = lazy(() =>\n import('./pages/WorkspaceSettingsPage').then((m) => ({ default: m.WorkspaceSettingsPage }))\n);\nconst ProjectsListPage = lazy(() =>\n import('./pages/ProjectsListPage').then((m) => ({ default: m.ProjectsListPage }))\n);\nconst ProjectDetailPage = lazy(() =>\n import('./pages/ProjectDetailPage').then((m) => ({ default: m.ProjectDetailPage }))\n);\nconst ProjectMembersPage = lazy(() =>\n import('./pages/ProjectMembersPage').then((m) => ({ default: m.ProjectMembersPage }))\n);\nconst MembersListPage = lazy(() =>\n import('./pages/MembersListPage').then((m) => ({ default: m.MembersListPage }))\n);\nconst ActivityListPage = lazy(() =>\n import('./pages/ActivityListPage').then((m) => ({ default: m.ActivityListPage }))\n);\nconst InviteAcceptancePage = lazy(() =>\n import('./pages/InviteAcceptancePage').then((m) => ({ default: m.InviteAcceptancePage }))\n);\nconst PendingInvitesPage = lazy(() =>\n import('./pages/PendingInvitesPage').then((m) => ({ default: m.PendingInvitesPage }))\n);\nconst WorkspaceInvitePage = lazy(() =>\n import('./pages/WorkspaceInvitePage').then((m) => ({ default: m.WorkspaceInvitePage }))\n);\n\n/**\n * Loading fallback for lazy-loaded components\n */\nfunction PageLoader() {\n return (\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"animate-spin rounded-full size-8 border-b-2 border-primary\" />\n </div>\n );\n}\n\n/**\n * Fallback shown when a feature-gated route is disabled.\n */\nfunction FeatureDisabledFallback() {\n return (\n <div className=\"flex h-full items-center justify-center p-8 text-center\">\n <div>\n <h2 className=\"text-lg font-semibold mb-2 text-text-primary\">Feature Not Available</h2>\n <p className=\"text-sm text-text-secondary\">\n This feature is not currently available. Enable it under Feature Previews to access this\n functionality.\n </p>\n </div>\n </div>\n );\n}\n\n/**\n * Helper: wraps a page element in a FeatureGate.\n * defaultValue=false → hidden until explicitly enabled (unreleased feature).\n */\nfunction gated(flag: string, element: React.ReactNode, defaultValue = false): React.ReactElement {\n return (\n <FeatureGate flag={flag} defaultValue={defaultValue} fallback={<FeatureDisabledFallback />}>\n {element}\n </FeatureGate>\n );\n}\n\n/**\n * Catch-all redirect component that uses the basePath from context\n * to navigate to an absolute path, preventing infinite redirect loops.\n */\nfunction CatchAllRedirect() {\n const { basePath = '/workspaces' } = useWorkspacesContext();\n return <Navigate to={`${basePath}/list`} replace />;\n}\n\nfunction CurrentWorkspaceInviteRedirect() {\n const { basePath = '/workspaces', workspaceId } = useWorkspacesContext();\n\n if (!workspaceId) {\n return <WorkspaceInvitePage />;\n }\n\n return <Navigate to={`${basePath}/${workspaceId}/invite`} replace />;\n}\n\n/**\n * Internal routing for the Workspaces microfrontend.\n * Defines all routes and their corresponding page components.\n *\n * - WorkspacesListPage: Eager loaded (main entry point)\n * - All other pages: Lazy loaded for better performance\n * - Projects, Members, Activity: gated by feature flags (unreleased)\n */\nexport function WorkspacesRoutes() {\n return (\n <Suspense fallback={<PageLoader />}>\n {/* Keeps the scoped `:workspaceId` URL segment in step with the active context\n when the user switches workspace via the chrome context switcher. */}\n <WorkspaceContextPathSync />\n <Routes>\n {/* Default redirect */}\n <Route index element={<Navigate to=\"list\" replace />} />\n\n {/* Workspaces List - Eager loaded */}\n <Route path=\"list\" element={<WorkspacesListPage />} />\n\n {/* Create Workspace (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"new\" element={<CreateWorkspacePage />} />\n\n {/* Pending Invites (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invites\" element={<PendingInvitesPage />} />\n\n {/* Workspace Invite */}\n <Route path=\"invite\" element={<CurrentWorkspaceInviteRedirect />} />\n\n {/* Invitation Acceptance (must come before :workspaceId to avoid conflicts) */}\n <Route path=\"invite/:invitationId\" element={<InviteAcceptancePage />} />\n\n {/* Workspace Detail */}\n <Route path=\":workspaceId\" element={<WorkspaceDetailPage />} />\n <Route\n path=\":workspaceId/settings\"\n element={gated('workspaces.settings', <WorkspaceSettingsPage />, true)}\n />\n <Route path=\":workspaceId/invite\" element={<WorkspaceInvitePage />} />\n\n {/* Projects — core feature, never gated */}\n <Route path=\"projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects\" element={<ProjectsListPage />} />\n <Route path=\":workspaceId/projects/:projectId\" element={<ProjectDetailPage />} />\n <Route path=\":workspaceId/projects/:projectId/members\" element={<ProjectMembersPage />} />\n\n {/* Members — core feature, never gated */}\n <Route path=\"members\" element={<MembersListPage />} />\n <Route path=\":workspaceId/members\" element={<MembersListPage />} />\n\n {/* Activity — core feature, never gated (BOFF-7247: the dashboard\n * Recent Activity widget links here, and the module-level flag\n * provider never fetches, so the gate always showed the fallback). */}\n <Route path=\"activity\" element={<ActivityListPage />} />\n <Route path=\":workspaceId/activity\" element={<ActivityListPage />} />\n\n {/* Catch-all redirect - uses absolute path to prevent infinite loops */}\n <Route path=\"*\" element={<CatchAllRedirect />} />\n </Routes>\n </Suspense>\n );\n}\n"],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@ var e = {
|
|
|
23
23
|
PROJECT_MEMBER_DELETE: "project-member:delete",
|
|
24
24
|
SETTINGS_READ: "settings:read",
|
|
25
25
|
SETTINGS_WRITE: "settings:write",
|
|
26
|
-
ACTIVITY_READ: "activity:read"
|
|
26
|
+
ACTIVITY_READ: "activity-log:read"
|
|
27
27
|
};
|
|
28
28
|
//#endregion
|
|
29
29
|
export { e as WORKSPACE_PERMISSIONS };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.js","names":[],"sources":["../../src/constants/permissions.ts"],"sourcesContent":["/**\n * Workspace MFE - Permission Constants\n *\n * Maps to action definitions in wspace-rbac-svc seed file.\n * Format: {resourceType}:{action}\n */\n\nexport const WORKSPACE_PERMISSIONS = {\n // Workspace\n WORKSPACE_LIST: 'workspace:list',\n WORKSPACE_READ: 'workspace:read',\n WORKSPACE_CREATE: 'workspace:create',\n WORKSPACE_UPDATE: 'workspace:update',\n WORKSPACE_DELETE: 'workspace:delete',\n WORKSPACE_ARCHIVE: 'workspace:archive',\n\n // Workspace Members\n MEMBER_LIST: 'workspace-member:list',\n MEMBER_READ: 'workspace-member:read',\n MEMBER_CREATE: 'workspace-member:create',\n MEMBER_UPDATE: 'workspace-member:update',\n MEMBER_DELETE: 'workspace-member:delete',\n\n // Projects\n PROJECT_LIST: 'project:list',\n PROJECT_READ: 'project:read',\n PROJECT_CREATE: 'project:create',\n PROJECT_UPDATE: 'project:update',\n PROJECT_DELETE: 'project:delete',\n PROJECT_ARCHIVE: 'project:archive',\n\n // Project Members\n PROJECT_MEMBER_LIST: 'project-member:list',\n PROJECT_MEMBER_READ: 'project-member:read',\n PROJECT_MEMBER_CREATE: 'project-member:create',\n PROJECT_MEMBER_DELETE: 'project-member:delete',\n\n // Settings\n SETTINGS_READ: 'settings:read',\n SETTINGS_WRITE: 'settings:write',\n\n // Activity / Audit\n ACTIVITY_READ: 'activity:read',\n} as const;\n"],"mappings":";AAOA,IAAa,IAAwB;CAEnC,gBAAgB;CAChB,gBAAgB;CAChB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,mBAAmB;CAGnB,aAAa;CACb,aAAa;CACb,eAAe;CACf,eAAe;CACf,eAAe;CAGf,cAAc;CACd,cAAc;CACd,gBAAgB;CAChB,gBAAgB;CAChB,gBAAgB;CAChB,iBAAiB;CAGjB,qBAAqB;CACrB,qBAAqB;CACrB,uBAAuB;CACvB,uBAAuB;CAGvB,eAAe;CACf,gBAAgB;
|
|
1
|
+
{"version":3,"file":"permissions.js","names":[],"sources":["../../src/constants/permissions.ts"],"sourcesContent":["/**\n * Workspace MFE - Permission Constants\n *\n * Maps to action definitions in wspace-rbac-svc seed file.\n * Format: {resourceType}:{action}\n */\n\nexport const WORKSPACE_PERMISSIONS = {\n // Workspace\n WORKSPACE_LIST: 'workspace:list',\n WORKSPACE_READ: 'workspace:read',\n WORKSPACE_CREATE: 'workspace:create',\n WORKSPACE_UPDATE: 'workspace:update',\n WORKSPACE_DELETE: 'workspace:delete',\n WORKSPACE_ARCHIVE: 'workspace:archive',\n\n // Workspace Members\n MEMBER_LIST: 'workspace-member:list',\n MEMBER_READ: 'workspace-member:read',\n MEMBER_CREATE: 'workspace-member:create',\n MEMBER_UPDATE: 'workspace-member:update',\n MEMBER_DELETE: 'workspace-member:delete',\n\n // Projects\n PROJECT_LIST: 'project:list',\n PROJECT_READ: 'project:read',\n PROJECT_CREATE: 'project:create',\n PROJECT_UPDATE: 'project:update',\n PROJECT_DELETE: 'project:delete',\n PROJECT_ARCHIVE: 'project:archive',\n\n // Project Members\n PROJECT_MEMBER_LIST: 'project-member:list',\n PROJECT_MEMBER_READ: 'project-member:read',\n PROJECT_MEMBER_CREATE: 'project-member:create',\n PROJECT_MEMBER_DELETE: 'project-member:delete',\n\n // Settings\n SETTINGS_READ: 'settings:read',\n SETTINGS_WRITE: 'settings:write',\n\n // Activity / Audit — resourceType is 'activity-log' in the rbac\n // action_definitions seed (BOFF-7247: 'activity:read' never matched).\n ACTIVITY_READ: 'activity-log:read',\n} as const;\n"],"mappings":";AAOA,IAAa,IAAwB;CAEnC,gBAAgB;CAChB,gBAAgB;CAChB,kBAAkB;CAClB,kBAAkB;CAClB,kBAAkB;CAClB,mBAAmB;CAGnB,aAAa;CACb,aAAa;CACb,eAAe;CACf,eAAe;CACf,eAAe;CAGf,cAAc;CACd,cAAc;CACd,gBAAgB;CAChB,gBAAgB;CAChB,gBAAgB;CAChB,iBAAiB;CAGjB,qBAAqB;CACrB,qBAAqB;CACrB,uBAAuB;CACvB,uBAAuB;CAGvB,eAAe;CACf,gBAAgB;CAIhB,eAAe;CAChB"}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { tWithFallback as e } from "../../utils/i18n.js";
|
|
2
2
|
import { DEFAULT_DASHBOARD_WIDGETS as t, DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT as n, WIDGET_DEFINITIONS as r, isWidgetAvailable as i, resolveWidgetCopy as a } from "./config.js";
|
|
3
3
|
import { nativeImpact as o } from "../../utils/nativeBridge.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { dropPositionFromPointer as s, reorderByDropIndex as c } from "./reorder.js";
|
|
5
|
+
import { useCallback as l, useEffect as u, useRef as d, useState as f } from "react";
|
|
6
|
+
import { ArrowDown as p, ArrowUp as m, GripVertical as h, Loader2 as g, RotateCcw as _, Save as v } from "lucide-react";
|
|
7
|
+
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
8
|
+
import { Badge as x, Button as S, Separator as C, Sheet as w, SheetContent as T, SheetDescription as E, SheetFooter as D, SheetHeader as O, SheetTitle as ee, Switch as te } from "@burdenoff/fe-libs/ui";
|
|
9
|
+
import { useI18n as ne } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
9
10
|
//#region src/pages/dashboard/DashboardCustomizer.tsx
|
|
10
11
|
function k({ open: k, onOpenChange: A, widgets: j, onSave: M, isSaving: N, productId: P = "workspaces" }) {
|
|
11
|
-
let { t: F } =
|
|
12
|
-
|
|
12
|
+
let { t: F } = ne(), I = l((t, n) => e(F, t, n), [F]), [L, R] = f(j), [z, B] = f(!1), [V, H] = f(null), [U, W] = f(null), [G, K] = f(""), q = d(/* @__PURE__ */ new Map()), J = d(null), Y = d(0);
|
|
13
|
+
u(() => {
|
|
13
14
|
k && (R([...j]), B(!1));
|
|
14
15
|
}, [k, j]);
|
|
15
|
-
let
|
|
16
|
+
let X = l((e) => {
|
|
16
17
|
R((t) => t.map((t) => t.widgetId === e ? {
|
|
17
18
|
...t,
|
|
18
19
|
enabled: !t.enabled
|
|
19
20
|
} : t)), B(!0);
|
|
20
|
-
}, []),
|
|
21
|
+
}, []), Z = l((e, t) => {
|
|
21
22
|
R((n) => {
|
|
22
23
|
let r = [...n].sort((e, t) => e.order - t.order), i = r.findIndex((t) => t.widgetId === e);
|
|
23
24
|
if (i < 0) return n;
|
|
@@ -32,96 +33,155 @@ function k({ open: k, onOpenChange: A, widgets: j, onSave: M, isSaving: N, produ
|
|
|
32
33
|
order: o
|
|
33
34
|
}, r;
|
|
34
35
|
}), B(!0);
|
|
35
|
-
}, []),
|
|
36
|
+
}, []), re = l(() => {
|
|
36
37
|
R([...n[P] ?? t]), B(!0);
|
|
37
|
-
}, [P]),
|
|
38
|
+
}, [P]), ie = l((e) => (t) => {
|
|
39
|
+
t ? q.current.set(e, t) : q.current.delete(e);
|
|
40
|
+
}, []), ae = l((e, t) => {
|
|
41
|
+
H(e), J.current = null, cancelAnimationFrame(Y.current), t.dataTransfer.effectAllowed = "move", t.dataTransfer.setData("text/plain", e);
|
|
42
|
+
}, []), oe = l((e, t) => {
|
|
43
|
+
!V || V === e || (t.preventDefault(), t.dataTransfer.dropEffect = "move", W(e), cancelAnimationFrame(Y.current), Y.current = requestAnimationFrame(() => {
|
|
44
|
+
let n = q.current.get(e);
|
|
45
|
+
if (!n) return;
|
|
46
|
+
let r = s(t.clientY, n.getBoundingClientRect()), i = J.current;
|
|
47
|
+
i && i.fromId === V && i.toId === e && i.position === r || (J.current = {
|
|
48
|
+
fromId: V,
|
|
49
|
+
toId: e,
|
|
50
|
+
position: r
|
|
51
|
+
}, R((t) => c(t, V, e, r)), B(!0));
|
|
52
|
+
}));
|
|
53
|
+
}, [V]), Q = l((e, t) => {
|
|
54
|
+
t.currentTarget.contains(t.relatedTarget) || U === e && W(null);
|
|
55
|
+
}, [U]), se = l((e, t) => {
|
|
56
|
+
t.preventDefault(), t.dataTransfer && (t.dataTransfer.dropEffect = "move"), cancelAnimationFrame(Y.current), H(null), W(null);
|
|
57
|
+
}, []), ce = l(() => {
|
|
58
|
+
if (cancelAnimationFrame(Y.current), V) {
|
|
59
|
+
let t = [...L].sort((e, t) => e.order - t.order), n = t.findIndex((e) => e.widgetId === V), i = r[V], o = i ? a(I, i).title : V;
|
|
60
|
+
n >= 0 && K(e(F, "dashboardCustomizer.reorderAnnouncement", `${o} moved to position ${n + 1} of ${t.length}`));
|
|
61
|
+
}
|
|
62
|
+
H(null), W(null), J.current = null;
|
|
63
|
+
}, [
|
|
64
|
+
V,
|
|
65
|
+
L,
|
|
66
|
+
I,
|
|
67
|
+
F
|
|
68
|
+
]), le = l(async () => {
|
|
38
69
|
await M(L), B(!1), A(!1);
|
|
39
70
|
}, [
|
|
40
71
|
L,
|
|
41
72
|
M,
|
|
42
73
|
A
|
|
43
|
-
]),
|
|
44
|
-
return /* @__PURE__ */
|
|
74
|
+
]), $ = [...L].sort((e, t) => e.order - t.order);
|
|
75
|
+
return /* @__PURE__ */ y(w, {
|
|
45
76
|
open: k,
|
|
46
77
|
onOpenChange: A,
|
|
47
|
-
children: /* @__PURE__ */
|
|
78
|
+
children: /* @__PURE__ */ b(T, {
|
|
48
79
|
className: "w-full sm:max-w-md overflow-y-auto",
|
|
49
80
|
side: "right",
|
|
50
81
|
children: [
|
|
51
|
-
/* @__PURE__ */
|
|
52
|
-
/* @__PURE__ */
|
|
82
|
+
/* @__PURE__ */ b(O, { children: [/* @__PURE__ */ y(ee, { children: e(F, "dashboardCustomizer.title", "Customize Dashboard") }), /* @__PURE__ */ y(E, { children: e(F, "dashboardCustomizer.description", "Toggle widgets on or off and reorder them to personalize your dashboard.") })] }),
|
|
83
|
+
/* @__PURE__ */ b("div", {
|
|
84
|
+
role: "listbox",
|
|
85
|
+
"aria-label": e(F, "dashboardCustomizer.listLabel", "Dashboard widgets"),
|
|
53
86
|
className: "py-4 space-y-1",
|
|
54
|
-
children:
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
children: [/* @__PURE__ */ y("div", {
|
|
88
|
+
"aria-live": "assertive",
|
|
89
|
+
className: "sr-only",
|
|
90
|
+
children: G
|
|
91
|
+
}), $.map((t, n) => {
|
|
92
|
+
let s = r[t.widgetId];
|
|
93
|
+
if (!s || !i(s, P)) return null;
|
|
94
|
+
let c = V === t.widgetId, l = U === t.widgetId && V !== null, u = a(I, s).title;
|
|
95
|
+
return /* @__PURE__ */ b("div", {
|
|
96
|
+
ref: ie(t.widgetId),
|
|
97
|
+
role: "option",
|
|
98
|
+
"aria-roledescription": e(F, "dashboardCustomizer.sortableItem", "sortable item"),
|
|
99
|
+
"aria-selected": c,
|
|
100
|
+
"aria-label": e(F, "dashboardCustomizer.widgetRow", `Widget: ${u}. Use the arrow buttons to reorder.`),
|
|
101
|
+
onDragOver: (e) => oe(t.widgetId, e),
|
|
102
|
+
onDragLeave: (e) => Q(t.widgetId, e),
|
|
103
|
+
onDrop: (e) => se(t.widgetId, e),
|
|
104
|
+
"data-widget-id": t.widgetId,
|
|
105
|
+
className: `flex items-center gap-3 p-3 rounded-lg border transition-colors ${t.enabled ? "border-border-default bg-bg-surface" : "border-transparent bg-bg-sunken/30 opacity-60"} ${c ? "opacity-50" : ""} ${l ? "ring-2 ring-border-strong border-transparent" : ""}`,
|
|
58
106
|
children: [
|
|
59
|
-
/* @__PURE__ */
|
|
60
|
-
|
|
107
|
+
/* @__PURE__ */ y("span", {
|
|
108
|
+
draggable: !0,
|
|
109
|
+
onDragStart: (e) => ae(t.widgetId, e),
|
|
110
|
+
onDragEnd: ce,
|
|
111
|
+
"aria-label": e(F, "dashboardCustomizer.dragHandle", `Drag handle for ${u}`),
|
|
112
|
+
className: "cursor-grab active:cursor-grabbing touch-none select-none",
|
|
113
|
+
children: /* @__PURE__ */ y(h, {
|
|
114
|
+
"aria-hidden": !0,
|
|
115
|
+
className: "size-4 text-text-secondary shrink-0"
|
|
116
|
+
})
|
|
117
|
+
}),
|
|
118
|
+
/* @__PURE__ */ b("div", {
|
|
61
119
|
className: "flex-1 min-w-0",
|
|
62
|
-
children: [/* @__PURE__ */
|
|
120
|
+
children: [/* @__PURE__ */ b("div", {
|
|
63
121
|
className: "flex items-center gap-2",
|
|
64
|
-
children: [/* @__PURE__ */
|
|
122
|
+
children: [/* @__PURE__ */ y("span", {
|
|
65
123
|
className: "text-sm font-medium text-text-primary",
|
|
66
|
-
children:
|
|
67
|
-
}), /* @__PURE__ */
|
|
124
|
+
children: u
|
|
125
|
+
}), /* @__PURE__ */ y(x, {
|
|
68
126
|
variant: "outline",
|
|
69
127
|
className: "text-[10px]",
|
|
70
|
-
children:
|
|
128
|
+
children: s.defaultSize
|
|
71
129
|
})]
|
|
72
|
-
}), /* @__PURE__ */
|
|
130
|
+
}), /* @__PURE__ */ y("p", {
|
|
73
131
|
className: "text-xs text-text-secondary truncate",
|
|
74
|
-
children: a(I,
|
|
132
|
+
children: a(I, s).description
|
|
75
133
|
})]
|
|
76
134
|
}),
|
|
77
|
-
/* @__PURE__ */
|
|
135
|
+
/* @__PURE__ */ b("div", {
|
|
78
136
|
className: "flex items-center gap-1 shrink-0",
|
|
79
137
|
children: [
|
|
80
|
-
/* @__PURE__ */
|
|
138
|
+
/* @__PURE__ */ y(S, {
|
|
81
139
|
variant: "ghost",
|
|
82
140
|
size: "sm",
|
|
83
141
|
className: "size-7 p-0",
|
|
84
|
-
disabled:
|
|
85
|
-
onClick: () =>
|
|
86
|
-
|
|
142
|
+
disabled: n === 0,
|
|
143
|
+
onClick: () => Z(t.widgetId, "up"),
|
|
144
|
+
"aria-label": e(F, "dashboardCustomizer.moveUp", `Move ${u} up`),
|
|
145
|
+
children: /* @__PURE__ */ y(m, { className: "size-3" })
|
|
87
146
|
}),
|
|
88
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ y(S, {
|
|
89
148
|
variant: "ghost",
|
|
90
149
|
size: "sm",
|
|
91
150
|
className: "size-7 p-0",
|
|
92
|
-
disabled:
|
|
93
|
-
onClick: () =>
|
|
94
|
-
|
|
151
|
+
disabled: n === $.length - 1,
|
|
152
|
+
onClick: () => Z(t.widgetId, "down"),
|
|
153
|
+
"aria-label": e(F, "dashboardCustomizer.moveDown", `Move ${u} down`),
|
|
154
|
+
children: /* @__PURE__ */ y(p, { className: "size-3" })
|
|
95
155
|
}),
|
|
96
|
-
/* @__PURE__ */
|
|
97
|
-
checked:
|
|
156
|
+
/* @__PURE__ */ y(te, {
|
|
157
|
+
checked: t.enabled,
|
|
98
158
|
onCheckedChange: () => {
|
|
99
|
-
o("medium"),
|
|
159
|
+
o("medium"), X(t.widgetId);
|
|
100
160
|
},
|
|
101
|
-
disabled: !
|
|
161
|
+
disabled: !s.removable,
|
|
102
162
|
className: "ml-1"
|
|
103
163
|
})
|
|
104
164
|
]
|
|
105
165
|
})
|
|
106
166
|
]
|
|
107
|
-
},
|
|
108
|
-
})
|
|
167
|
+
}, t.widgetId);
|
|
168
|
+
})]
|
|
109
169
|
}),
|
|
110
|
-
/* @__PURE__ */
|
|
111
|
-
/* @__PURE__ */
|
|
170
|
+
/* @__PURE__ */ y(C, {}),
|
|
171
|
+
/* @__PURE__ */ b(D, {
|
|
112
172
|
className: "flex-row gap-2 pt-4",
|
|
113
|
-
children: [/* @__PURE__ */
|
|
173
|
+
children: [/* @__PURE__ */ b(S, {
|
|
114
174
|
variant: "outline",
|
|
115
175
|
size: "sm",
|
|
116
|
-
onClick:
|
|
176
|
+
onClick: re,
|
|
117
177
|
className: "flex-1",
|
|
118
|
-
children: [/* @__PURE__ */
|
|
119
|
-
}), /* @__PURE__ */
|
|
178
|
+
children: [/* @__PURE__ */ y(_, { className: "size-3 mr-1.5" }), e(F, "dashboardCustomizer.resetToDefaults", "Reset to defaults")]
|
|
179
|
+
}), /* @__PURE__ */ b(S, {
|
|
120
180
|
size: "sm",
|
|
121
|
-
onClick:
|
|
181
|
+
onClick: le,
|
|
122
182
|
disabled: !z || N,
|
|
123
183
|
className: "flex-1",
|
|
124
|
-
children: [N ? /* @__PURE__ */ g
|
|
184
|
+
children: [N ? /* @__PURE__ */ y(g, { className: "size-3 mr-1.5 animate-spin" }) : /* @__PURE__ */ y(v, { className: "size-3 mr-1.5" }), e(F, "dashboardCustomizer.saveLayout", "Save layout")]
|
|
125
185
|
})]
|
|
126
186
|
})
|
|
127
187
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardCustomizer.js","names":[],"sources":["../../../src/pages/dashboard/DashboardCustomizer.tsx"],"sourcesContent":["/**\n * DashboardCustomizer — Sheet overlay for customizing dashboard widgets.\n *\n * Allows users to toggle widgets on/off, reorder them, and save their layout.\n */\nimport { useState, useCallback, useEffect } from 'react';\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetDescription,\n SheetFooter,\n Button,\n Switch,\n Badge,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { ArrowUp, ArrowDown, RotateCcw, Save, Loader2, GripVertical } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../../utils/i18n';\nimport type { WidgetConfig } from './types';\nimport {\n WIDGET_DEFINITIONS,\n DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT,\n DEFAULT_DASHBOARD_WIDGETS,\n isWidgetAvailable,\n resolveWidgetCopy,\n} from './config';\nimport { nativeImpact } from '../../utils/nativeBridge';\n\ninterface DashboardCustomizerProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n widgets: WidgetConfig[];\n onSave: (widgets: WidgetConfig[]) => Promise<void>;\n isSaving: boolean;\n productId?: string;\n}\n\nexport function DashboardCustomizer({\n open,\n onOpenChange,\n widgets,\n onSave,\n isSaving,\n productId = 'workspaces',\n}: DashboardCustomizerProps) {\n const { t } = useI18n();\n // Widget titles/descriptions live in module-scope WIDGET_DEFINITIONS, so they\n // are translated here at render time with literal keys. [BOFF-5997]\n const trLiteral = (key: string, fallback: string): string => tWithFallback(t, key, fallback);\n\n const [localWidgets, setLocalWidgets] = useState<WidgetConfig[]>(widgets);\n const [hasChanges, setHasChanges] = useState(false);\n\n // Sync local state when sheet opens or widgets prop changes while open\n useEffect(() => {\n if (open) {\n setLocalWidgets([...widgets]);\n setHasChanges(false);\n }\n }, [open, widgets]);\n\n const toggleWidget = useCallback((widgetId: string) => {\n setLocalWidgets((prev) =>\n prev.map((w) => (w.widgetId === widgetId ? { ...w, enabled: !w.enabled } : w))\n );\n setHasChanges(true);\n }, []);\n\n const moveWidget = useCallback((widgetId: string, direction: 'up' | 'down') => {\n setLocalWidgets((prev) => {\n const sorted = [...prev].sort((a, b) => a.order - b.order);\n const idx = sorted.findIndex((w) => w.widgetId === widgetId);\n if (idx < 0) return prev;\n\n const swapIdx = direction === 'up' ? idx - 1 : idx + 1;\n if (swapIdx < 0 || swapIdx >= sorted.length) return prev;\n\n // Swap orders\n const tempOrder = sorted[idx].order;\n sorted[idx] = { ...sorted[idx], order: sorted[swapIdx].order };\n sorted[swapIdx] = { ...sorted[swapIdx], order: tempOrder };\n\n return sorted;\n });\n setHasChanges(true);\n }, []);\n\n const resetToDefaults = useCallback(() => {\n const defaults = DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT[productId] ?? DEFAULT_DASHBOARD_WIDGETS;\n setLocalWidgets([...defaults]);\n setHasChanges(true);\n }, [productId]);\n\n const handleSave = useCallback(async () => {\n await onSave(localWidgets);\n setHasChanges(false);\n onOpenChange(false);\n }, [localWidgets, onSave, onOpenChange]);\n\n const sorted = [...localWidgets].sort((a, b) => a.order - b.order);\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent className=\"w-full sm:max-w-md overflow-y-auto\" side=\"right\">\n <SheetHeader>\n <SheetTitle>\n {tWithFallback(t, 'dashboardCustomizer.title', 'Customize Dashboard')}\n </SheetTitle>\n <SheetDescription>\n {tWithFallback(\n t,\n 'dashboardCustomizer.description',\n 'Toggle widgets on or off and reorder them to personalize your dashboard.'\n )}\n </SheetDescription>\n </SheetHeader>\n\n <div className=\"py-4 space-y-1\">\n {sorted.map((widget, idx) => {\n const definition = WIDGET_DEFINITIONS[widget.widgetId];\n if (!definition) return null;\n if (!isWidgetAvailable(definition, productId)) return null;\n\n return (\n <div\n key={widget.widgetId}\n className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${\n widget.enabled\n ? 'border-border-default bg-bg-surface'\n : 'border-transparent bg-bg-sunken/30 opacity-60'\n }`}\n >\n <GripVertical className=\"size-4 text-text-secondary shrink-0\" />\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">\n {resolveWidgetCopy(trLiteral, definition).title}\n </span>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {definition.defaultSize}\n </Badge>\n </div>\n <p className=\"text-xs text-text-secondary truncate\">\n {resolveWidgetCopy(trLiteral, definition).description}\n </p>\n </div>\n\n <div className=\"flex items-center gap-1 shrink-0\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === 0}\n onClick={() => moveWidget(widget.widgetId, 'up')}\n >\n <ArrowUp className=\"size-3\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === sorted.length - 1}\n onClick={() => moveWidget(widget.widgetId, 'down')}\n >\n <ArrowDown className=\"size-3\" />\n </Button>\n <Switch\n checked={widget.enabled}\n onCheckedChange={() => {\n void nativeImpact('medium');\n toggleWidget(widget.widgetId);\n }}\n disabled={!definition.removable}\n className=\"ml-1\"\n />\n </div>\n </div>\n );\n })}\n </div>\n\n <Separator />\n\n <SheetFooter className=\"flex-row gap-2 pt-4\">\n <Button variant=\"outline\" size=\"sm\" onClick={resetToDefaults} className=\"flex-1\">\n <RotateCcw className=\"size-3 mr-1.5\" />\n {tWithFallback(t, 'dashboardCustomizer.resetToDefaults', 'Reset to defaults')}\n </Button>\n <Button\n size=\"sm\"\n onClick={handleSave}\n disabled={!hasChanges || isSaving}\n className=\"flex-1\"\n >\n {isSaving ? (\n <Loader2 className=\"size-3 mr-1.5 animate-spin\" />\n ) : (\n <Save className=\"size-3 mr-1.5\" />\n )}\n {tWithFallback(t, 'dashboardCustomizer.saveLayout', 'Save layout')}\n </Button>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardCustomizer.js","names":[],"sources":["../../../src/pages/dashboard/DashboardCustomizer.tsx"],"sourcesContent":["/**\n * DashboardCustomizer — Sheet overlay for customizing dashboard widgets.\n *\n * Allows users to toggle widgets on/off, reorder them, and save their layout.\n */\nimport { useState, useCallback, useEffect, useRef } from 'react';\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n SheetDescription,\n SheetFooter,\n Button,\n Switch,\n Badge,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { ArrowUp, ArrowDown, RotateCcw, Save, Loader2, GripVertical } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { tWithFallback } from '../../utils/i18n';\nimport type { WidgetConfig } from './types';\nimport {\n WIDGET_DEFINITIONS,\n DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT,\n DEFAULT_DASHBOARD_WIDGETS,\n isWidgetAvailable,\n resolveWidgetCopy,\n} from './config';\nimport { nativeImpact } from '../../utils/nativeBridge';\nimport { reorderByDropIndex, dropPositionFromPointer, type DropPosition } from './reorder';\n\ninterface DashboardCustomizerProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n widgets: WidgetConfig[];\n onSave: (widgets: WidgetConfig[]) => Promise<void>;\n isSaving: boolean;\n productId?: string;\n}\n\nexport function DashboardCustomizer({\n open,\n onOpenChange,\n widgets,\n onSave,\n isSaving,\n productId = 'workspaces',\n}: DashboardCustomizerProps) {\n const { t } = useI18n();\n // Widget titles/descriptions live in module-scope WIDGET_DEFINITIONS, so they\n // are translated here at render time with literal keys. [BOFF-5997]\n const trLiteral = useCallback(\n (key: string, fallback: string): string => tWithFallback(t, key, fallback),\n [t]\n );\n\n const [localWidgets, setLocalWidgets] = useState<WidgetConfig[]>(widgets);\n const [hasChanges, setHasChanges] = useState(false);\n const [draggingId, setDraggingId] = useState<string | null>(null);\n const [dropTargetId, setDropTargetId] = useState<string | null>(null);\n /** Live-region announcement for screen readers after a successful reorder. */\n const [liveAnnouncement, setLiveAnnouncement] = useState('');\n const rowRefs = useRef(new Map<string, HTMLDivElement>());\n /** Last (from, to, position) applied during the current drag — used to skip\n * repeated `dragOver` firings that would otherwise churn renders. */\n const lastAppliedMoveRef = useRef<{\n fromId: string;\n toId: string;\n position: DropPosition;\n } | null>(null);\n /** Pending rAF id for coalescing `dragOver` reorders to one per frame. */\n const dragOverRafRef = useRef(0);\n\n // Sync local state when sheet opens or widgets prop changes while open\n useEffect(() => {\n if (open) {\n setLocalWidgets([...widgets]);\n setHasChanges(false);\n }\n }, [open, widgets]);\n\n const toggleWidget = useCallback((widgetId: string) => {\n setLocalWidgets((prev) =>\n prev.map((w) => (w.widgetId === widgetId ? { ...w, enabled: !w.enabled } : w))\n );\n setHasChanges(true);\n }, []);\n\n const moveWidget = useCallback((widgetId: string, direction: 'up' | 'down') => {\n setLocalWidgets((prev) => {\n const sorted = [...prev].sort((a, b) => a.order - b.order);\n const idx = sorted.findIndex((w) => w.widgetId === widgetId);\n if (idx < 0) return prev;\n\n const swapIdx = direction === 'up' ? idx - 1 : idx + 1;\n if (swapIdx < 0 || swapIdx >= sorted.length) return prev;\n\n // Swap orders\n const tempOrder = sorted[idx].order;\n sorted[idx] = { ...sorted[idx], order: sorted[swapIdx].order };\n sorted[swapIdx] = { ...sorted[swapIdx], order: tempOrder };\n\n return sorted;\n });\n setHasChanges(true);\n }, []);\n\n const resetToDefaults = useCallback(() => {\n const defaults = DEFAULT_DASHBOARD_WIDGETS_BY_PRODUCT[productId] ?? DEFAULT_DASHBOARD_WIDGETS;\n setLocalWidgets([...defaults]);\n setHasChanges(true);\n }, [productId]);\n\n // ─── Drag-and-drop (HTML5 native) ──────────────────────────────────────────\n // The 6-dot icon next to each widget is the drag handle. Up/down arrow\n // buttons remain the keyboard-accessible reorder path. [BOFF-7219]\n const setRowRef = useCallback(\n (widgetId: string) => (el: HTMLDivElement | null) => {\n if (el) rowRefs.current.set(widgetId, el);\n else rowRefs.current.delete(widgetId);\n },\n []\n );\n\n const handleDragStart = useCallback(\n (widgetId: string, event: React.DragEvent<HTMLSpanElement>) => {\n setDraggingId(widgetId);\n lastAppliedMoveRef.current = null;\n cancelAnimationFrame(dragOverRafRef.current);\n event.dataTransfer.effectAllowed = 'move';\n // Firefox refuses to fire drag events without a setData payload.\n event.dataTransfer.setData('text/plain', widgetId);\n },\n []\n );\n\n const handleDragOver = useCallback(\n (widgetId: string, event: React.DragEvent<HTMLDivElement>) => {\n if (!draggingId || draggingId === widgetId) return;\n event.preventDefault();\n event.dataTransfer.dropEffect = 'move';\n setDropTargetId(widgetId);\n\n // Coalesce the expensive reorder work to once per animation frame.\n // `dragOver` fires at ~60 FPS even when the pointer is stationary;\n // without this guard every firing triggers a re-sort + re-render.\n cancelAnimationFrame(dragOverRafRef.current);\n dragOverRafRef.current = requestAnimationFrame(() => {\n const row = rowRefs.current.get(widgetId);\n if (!row) return;\n const position = dropPositionFromPointer(event.clientY, row.getBoundingClientRect());\n const last = lastAppliedMoveRef.current;\n if (\n last &&\n last.fromId === draggingId &&\n last.toId === widgetId &&\n last.position === position\n ) {\n return; // same move as last firing — skip to avoid re-render churn\n }\n lastAppliedMoveRef.current = { fromId: draggingId, toId: widgetId, position };\n setLocalWidgets((prev) => reorderByDropIndex(prev, draggingId, widgetId, position));\n setHasChanges(true);\n });\n },\n [draggingId]\n );\n\n const handleDragLeave = useCallback(\n (widgetId: string, event: React.DragEvent<HTMLDivElement>) => {\n // Only clear the drop target when the pointer truly leaves the row\n // (entering a child element bubbles dragleave on the parent).\n if (event.currentTarget.contains(event.relatedTarget as Node | null)) return;\n if (dropTargetId === widgetId) setDropTargetId(null);\n },\n [dropTargetId]\n );\n\n const handleDrop = useCallback((widgetId: string, event: React.DragEvent<HTMLDivElement>) => {\n event.preventDefault();\n if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';\n cancelAnimationFrame(dragOverRafRef.current);\n setDraggingId(null);\n setDropTargetId(null);\n }, []);\n\n const handleDragEnd = useCallback(() => {\n cancelAnimationFrame(dragOverRafRef.current);\n // Announce the final position of the dragged widget for screen-reader\n // users. aria-live=\"assertive\" reads the new text immediately.\n if (draggingId) {\n const finalWidgets = [...localWidgets].sort((a, b) => a.order - b.order);\n const finalIdx = finalWidgets.findIndex((w) => w.widgetId === draggingId);\n const definition = WIDGET_DEFINITIONS[draggingId];\n const title = definition ? resolveWidgetCopy(trLiteral, definition).title : draggingId;\n if (finalIdx >= 0) {\n setLiveAnnouncement(\n tWithFallback(\n t,\n 'dashboardCustomizer.reorderAnnouncement',\n `${title} moved to position ${finalIdx + 1} of ${finalWidgets.length}`\n )\n );\n }\n }\n setDraggingId(null);\n setDropTargetId(null);\n lastAppliedMoveRef.current = null;\n }, [draggingId, localWidgets, trLiteral, t]);\n\n const handleSave = useCallback(async () => {\n await onSave(localWidgets);\n setHasChanges(false);\n onOpenChange(false);\n }, [localWidgets, onSave, onOpenChange]);\n\n const sorted = [...localWidgets].sort((a, b) => a.order - b.order);\n\n return (\n <Sheet open={open} onOpenChange={onOpenChange}>\n <SheetContent className=\"w-full sm:max-w-md overflow-y-auto\" side=\"right\">\n <SheetHeader>\n <SheetTitle>\n {tWithFallback(t, 'dashboardCustomizer.title', 'Customize Dashboard')}\n </SheetTitle>\n <SheetDescription>\n {tWithFallback(\n t,\n 'dashboardCustomizer.description',\n 'Toggle widgets on or off and reorder them to personalize your dashboard.'\n )}\n </SheetDescription>\n </SheetHeader>\n\n <div\n role=\"listbox\"\n aria-label={tWithFallback(t, 'dashboardCustomizer.listLabel', 'Dashboard widgets')}\n className=\"py-4 space-y-1\"\n >\n {/* Live region for screen-reader announcements after a successful\n reorder. `aria-live=\"assertive\"` interrupts the user to make\n the new position obvious; the region itself is visually\n hidden via `sr-only`. */}\n <div aria-live=\"assertive\" className=\"sr-only\">\n {liveAnnouncement}\n </div>\n {sorted.map((widget, idx) => {\n const definition = WIDGET_DEFINITIONS[widget.widgetId];\n if (!definition) return null;\n if (!isWidgetAvailable(definition, productId)) return null;\n\n const isDragging = draggingId === widget.widgetId;\n const isDropTarget = dropTargetId === widget.widgetId && draggingId !== null;\n const widgetTitle = resolveWidgetCopy(trLiteral, definition).title;\n\n return (\n <div\n key={widget.widgetId}\n ref={setRowRef(widget.widgetId)}\n role=\"option\"\n aria-roledescription={tWithFallback(\n t,\n 'dashboardCustomizer.sortableItem',\n 'sortable item'\n )}\n aria-selected={isDragging}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.widgetRow',\n `Widget: ${widgetTitle}. Use the arrow buttons to reorder.`\n )}\n onDragOver={(e) => handleDragOver(widget.widgetId, e)}\n onDragLeave={(e) => handleDragLeave(widget.widgetId, e)}\n onDrop={(e) => handleDrop(widget.widgetId, e)}\n data-widget-id={widget.widgetId}\n className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${\n widget.enabled\n ? 'border-border-default bg-bg-surface'\n : 'border-transparent bg-bg-sunken/30 opacity-60'\n } ${isDragging ? 'opacity-50' : ''} ${\n isDropTarget ? 'ring-2 ring-border-strong border-transparent' : ''\n }`}\n >\n <span\n draggable\n onDragStart={(e) => handleDragStart(widget.widgetId, e)}\n onDragEnd={handleDragEnd}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.dragHandle',\n `Drag handle for ${widgetTitle}`\n )}\n className=\"cursor-grab active:cursor-grabbing touch-none select-none\"\n >\n <GripVertical aria-hidden className=\"size-4 text-text-secondary shrink-0\" />\n </span>\n\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">{widgetTitle}</span>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {definition.defaultSize}\n </Badge>\n </div>\n <p className=\"text-xs text-text-secondary truncate\">\n {resolveWidgetCopy(trLiteral, definition).description}\n </p>\n </div>\n\n <div className=\"flex items-center gap-1 shrink-0\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === 0}\n onClick={() => moveWidget(widget.widgetId, 'up')}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.moveUp',\n `Move ${widgetTitle} up`\n )}\n >\n <ArrowUp className=\"size-3\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"size-7 p-0\"\n disabled={idx === sorted.length - 1}\n onClick={() => moveWidget(widget.widgetId, 'down')}\n aria-label={tWithFallback(\n t,\n 'dashboardCustomizer.moveDown',\n `Move ${widgetTitle} down`\n )}\n >\n <ArrowDown className=\"size-3\" />\n </Button>\n <Switch\n checked={widget.enabled}\n onCheckedChange={() => {\n void nativeImpact('medium');\n toggleWidget(widget.widgetId);\n }}\n disabled={!definition.removable}\n className=\"ml-1\"\n />\n </div>\n </div>\n );\n })}\n </div>\n\n <Separator />\n\n <SheetFooter className=\"flex-row gap-2 pt-4\">\n <Button variant=\"outline\" size=\"sm\" onClick={resetToDefaults} className=\"flex-1\">\n <RotateCcw className=\"size-3 mr-1.5\" />\n {tWithFallback(t, 'dashboardCustomizer.resetToDefaults', 'Reset to defaults')}\n </Button>\n <Button\n size=\"sm\"\n onClick={handleSave}\n disabled={!hasChanges || isSaving}\n className=\"flex-1\"\n >\n {isSaving ? (\n <Loader2 className=\"size-3 mr-1.5 animate-spin\" />\n ) : (\n <Save className=\"size-3 mr-1.5\" />\n )}\n {tWithFallback(t, 'dashboardCustomizer.saveLayout', 'Save layout')}\n </Button>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n );\n}\n"],"mappings":"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"}
|
|
@@ -42,15 +42,21 @@ function l(e) {
|
|
|
42
42
|
],
|
|
43
43
|
enabled: !!e && !!n,
|
|
44
44
|
queryFn: async () => {
|
|
45
|
-
let n =
|
|
45
|
+
let [n, r] = await Promise.all([o({
|
|
46
46
|
gateway: "workspace",
|
|
47
47
|
authToken: e || void 0,
|
|
48
48
|
workspaceToken: !0,
|
|
49
49
|
query: a(t),
|
|
50
50
|
suppressGlobalErrorEvent: !0
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
}), o({
|
|
52
|
+
gateway: "workspace",
|
|
53
|
+
authToken: e || void 0,
|
|
54
|
+
workspaceToken: !0,
|
|
55
|
+
query: "query DashboardRecentActivities { getRecentActivities(limit: 8) { id action resourceType resourceId userId description details timestamp } }",
|
|
56
|
+
suppressGlobalErrorEvent: !0
|
|
57
|
+
})]), i = n.data, c = i?.workspaceResourceUsage ? {
|
|
58
|
+
usedBytes: Math.round(i.workspaceResourceUsage.currentStorageGb * s),
|
|
59
|
+
totalBytes: Math.round((i.workspaceResourceLimits?.maxStorageGb ?? 0) * s),
|
|
54
60
|
fileCount: null
|
|
55
61
|
} : null;
|
|
56
62
|
return {
|
|
@@ -62,22 +68,22 @@ function l(e) {
|
|
|
62
68
|
items: [],
|
|
63
69
|
total: 0
|
|
64
70
|
},
|
|
65
|
-
recentActivities:
|
|
66
|
-
upcomingEvents:
|
|
67
|
-
tags:
|
|
71
|
+
recentActivities: r.data?.getRecentActivities ?? [],
|
|
72
|
+
upcomingEvents: i?.upcomingEvents ?? [],
|
|
73
|
+
tags: i?.getTags ?? {
|
|
68
74
|
items: [],
|
|
69
75
|
total: 0
|
|
70
76
|
},
|
|
71
77
|
groups: {
|
|
72
|
-
items: (
|
|
78
|
+
items: (i?.myGroups?.edges ?? []).map((e) => ({
|
|
73
79
|
id: e.node.id,
|
|
74
80
|
name: e.node.name,
|
|
75
81
|
description: e.node.description
|
|
76
82
|
})),
|
|
77
|
-
totalCount:
|
|
83
|
+
totalCount: i?.myGroups?.totalCount ?? 0
|
|
78
84
|
},
|
|
79
|
-
workflowStats:
|
|
80
|
-
storageUsage:
|
|
85
|
+
workflowStats: i?.dashboardSummary ?? null,
|
|
86
|
+
storageUsage: c
|
|
81
87
|
};
|
|
82
88
|
},
|
|
83
89
|
staleTime: 120 * 1e3,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useDashboardData.js","names":[],"sources":["../../../../src/pages/dashboard/hooks/useDashboardData.ts"],"sourcesContent":["/**\n * Dashboard Data Hooks\n *\n * Three parallel queries: workspace gateway (features + projects), global gateway.\n * Uses graphqlFetch from fe-libs with React Query for caching.\n *\n * IMPORTANT: Workspace/org/member data lives in the global tenant service\n * (global gateway), NOT in the workspace service (workspace gateway).\n * The workspace gateway is used for workspace-scoped features like\n * activity, calendar, tags, groups, projects, and workflows.\n *\n * All GraphQL operations are defined in src/operations/ and generated via codegen.\n * Document nodes are imported and converted to strings via print() for graphqlFetch.\n */\nimport { useQuery } from '@tanstack/react-query';\nimport { print } from 'graphql';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n DashboardProjectsDocument,\n DashboardWorkspaceDataDocument,\n} from '../../../generated/wspace-operations';\nimport { DashboardGlobalDataDocument } from '../../../generated/global-operations';\nimport type {\n WorkspaceDashboardData,\n GlobalDashboardData,\n VibeSummary,\n SessionSummary,\n AgentFleetSummary,\n} from '../types';\n\nconst BYTES_PER_GB = 1024 ** 3;\n\n// ─── Hooks ────────────────────────────────────────────────────────────────────\n\nexport function useProjectsDashboardData(authToken?: string | null) {\n const { workspaceId } = useActiveContext();\n\n return useQuery({\n queryKey: ['dashboard', 'projects', workspaceId],\n enabled: !!authToken && !!workspaceId,\n queryFn: async (): Promise<WorkspaceDashboardData['projects']> => {\n const result = await graphqlFetch<{\n projects: WorkspaceDashboardData['projects'];\n }>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query: print(DashboardProjectsDocument),\n variables: { filter: { workspaceId } },\n // Dashboard widgets degrade gracefully (empty state) when a subgraph\n // is unavailable; a transient backend failure here is not actionable\n // for the user, so suppress the global error toast/console log.\n suppressGlobalErrorEvent: true,\n });\n\n return result.data?.projects ?? { items: [], total: 0 };\n },\n staleTime: 2 * 60 * 1000,\n gcTime: 5 * 60 * 1000,\n retry: 1,\n refetchOnWindowFocus: true,\n });\n}\n\nexport function useWorkspaceDashboardData(authToken?: string | null) {\n const { workspaceId } = useActiveContext();\n\n return useQuery({\n queryKey: ['dashboard', 'workspace', workspaceId],\n enabled: !!authToken && !!workspaceId,\n queryFn: async (): Promise<WorkspaceDashboardData> => {\n const result = await graphqlFetch<{\n getRecentActivities: WorkspaceDashboardData['recentActivities'];\n upcomingEvents: WorkspaceDashboardData['upcomingEvents'];\n getTags: WorkspaceDashboardData['tags'];\n myGroups: {\n edges: Array<{ node: { id: string; name: string; description?: string } }>;\n totalCount: number;\n };\n dashboardSummary: WorkspaceDashboardData['workflowStats'];\n workspaceResourceUsage: { currentStorageGb: number } | null;\n workspaceResourceLimits: { maxStorageGb?: number | null } | null;\n }>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query: print(DashboardWorkspaceDataDocument),\n // This combined query fans out across several workspace subgraphs\n // (activity, calendar, tags, groups, workflow, files). A single\n // subgraph 500 nulls the whole response, but every field below is\n // null-coalesced to an empty state, so the dashboard still renders.\n // Suppress the global error toast/console log — a degraded subgraph\n // is not user-actionable here.\n suppressGlobalErrorEvent: true,\n });\n\n // Gracefully handle partial failures — the gateway may return data for some\n // subgraphs and errors for others due to individual services being down.\n const data = result.data;\n const storageUsage = data?.workspaceResourceUsage\n ? {\n usedBytes: Math.round(data.workspaceResourceUsage.currentStorageGb * BYTES_PER_GB),\n totalBytes: Math.round(\n (data.workspaceResourceLimits?.maxStorageGb ?? 0) * BYTES_PER_GB\n ),\n fileCount: null,\n }\n : null;\n\n return {\n // Workspace data comes from the global gateway (see useGlobalDashboardData)\n // and is merged in at the DashboardPage level.\n workspaces: { items: [], total: 0 },\n // Projects fetched separately via useProjectsDashboardData to avoid\n // being poisoned by other subgraph errors in the combined query.\n projects: { items: [], total: 0 },\n recentActivities: data?.getRecentActivities ?? [],\n upcomingEvents: data?.upcomingEvents ?? [],\n tags: data?.getTags ?? { items: [], total: 0 },\n groups: {\n items: (data?.myGroups?.edges ?? []).map((e) => ({\n id: e.node.id,\n name: e.node.name,\n description: e.node.description,\n })),\n totalCount: data?.myGroups?.totalCount ?? 0,\n },\n workflowStats: data?.dashboardSummary ?? null,\n storageUsage,\n };\n },\n staleTime: 2 * 60 * 1000, // 2 minutes — dashboard data changes moderately\n gcTime: 5 * 60 * 1000,\n retry: 1,\n refetchOnWindowFocus: true,\n });\n}\n\n// ─── VibeControls-specific dashboard data ────────────────────────────────────\n// Issued as raw GraphQL strings to avoid coupling to codegen for the new\n// `recentVibes` + `agentFleetSummary` queries (added in wspace-vibecontrols-svc).\n// Falls back to empty state if any field is missing in the deployed schema,\n// so the dashboard degrades gracefully during schema rollout.\n\nconst VIBECONTROLS_DASHBOARD_QUERY = `\n query VibeControlsDashboardData {\n recentVibes(limit: 5) {\n id\n name\n status\n workspaceId\n updatedAt\n }\n activeSessions(pagination: { limit: 5 }) {\n items {\n id\n name\n status\n agent { id name }\n startedAt\n vibeId\n }\n }\n agentFleetSummary {\n total\n online\n offline\n degraded\n lastSeenAt\n }\n }\n`;\n\ninterface VibecontrolsDashboardResponse {\n recentVibes?: VibeSummary[] | null;\n activeSessions?: { items?: SessionSummary[] | null } | null;\n agentFleetSummary?: AgentFleetSummary | null;\n}\n\nexport interface VibecontrolsDashboardData {\n recentVibes: VibeSummary[];\n activeSessions: SessionSummary[];\n agentFleet: AgentFleetSummary | null;\n}\n\nexport function useVibecontrolsDashboardData(authToken?: string | null, enabled = false) {\n const { workspaceId } = useActiveContext();\n\n return useQuery({\n queryKey: ['dashboard', 'vibecontrols', workspaceId],\n enabled: !!authToken && !!workspaceId && enabled,\n queryFn: async (): Promise<VibecontrolsDashboardData> => {\n try {\n const result = await graphqlFetch<VibecontrolsDashboardResponse>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query: VIBECONTROLS_DASHBOARD_QUERY,\n // Schema may not be deployed in this environment; handled locally\n // (empty fallback below). Suppress the global error toast/log.\n suppressGlobalErrorEvent: true,\n });\n const data = result.data;\n return {\n recentVibes: data?.recentVibes ?? [],\n activeSessions: data?.activeSessions?.items ?? [],\n agentFleet: data?.agentFleetSummary ?? null,\n };\n } catch {\n // Schema may not yet be deployed in this environment — render empty.\n return { recentVibes: [], activeSessions: [], agentFleet: null };\n }\n },\n staleTime: 60 * 1000,\n gcTime: 5 * 60 * 1000,\n retry: 0,\n refetchOnWindowFocus: true,\n });\n}\n\nexport function useGlobalDashboardData(authToken?: string | null) {\n return useQuery({\n queryKey: ['dashboard', 'global'],\n enabled: !!authToken,\n queryFn: async (): Promise<GlobalDashboardData> => {\n const result = await graphqlFetch<{\n currentUser: GlobalDashboardData['currentUser'];\n myDashboardPreferences: Record<string, unknown> | null;\n myWorkspaces: {\n items: Array<{\n id: string;\n name: string;\n status: string;\n type: string;\n createdAt: string;\n memberCount: number;\n }>;\n total: number;\n };\n myOrganizations: {\n items: Array<{\n id: string;\n name: string;\n status: string;\n type: string;\n createdAt: string;\n }>;\n total: number;\n };\n }>({\n gateway: 'global',\n authToken: authToken || undefined,\n query: print(DashboardGlobalDataDocument),\n // Dashboard data is null-coalesced to empty state below; a transient\n // subgraph failure should not raise a user-facing error here.\n suppressGlobalErrorEvent: true,\n });\n\n const data = result.data;\n\n return {\n currentUser: data?.currentUser ?? null,\n dashboardPreferences: data?.myDashboardPreferences\n ? (data.myDashboardPreferences as unknown as GlobalDashboardData['dashboardPreferences'])\n : null,\n workspaces: data?.myWorkspaces ?? { items: [], total: 0 },\n organizations: data?.myOrganizations ?? { items: [], total: 0 },\n };\n },\n staleTime: 5 * 60 * 1000, // 5 minutes — user info and prefs change rarely\n gcTime: 10 * 60 * 1000,\n retry: 1,\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useDashboardData.js","names":[],"sources":["../../../../src/pages/dashboard/hooks/useDashboardData.ts"],"sourcesContent":["/**\n * Dashboard Data Hooks\n *\n * Three parallel queries: workspace gateway (features + projects), global gateway.\n * Uses graphqlFetch from fe-libs with React Query for caching.\n *\n * IMPORTANT: Workspace/org/member data lives in the global tenant service\n * (global gateway), NOT in the workspace service (workspace gateway).\n * The workspace gateway is used for workspace-scoped features like\n * activity, calendar, tags, groups, projects, and workflows.\n *\n * All GraphQL operations are defined in src/operations/ and generated via codegen.\n * Document nodes are imported and converted to strings via print() for graphqlFetch.\n */\nimport { useQuery } from '@tanstack/react-query';\nimport { print } from 'graphql';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useActiveContext } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n DashboardProjectsDocument,\n DashboardWorkspaceDataDocument,\n} from '../../../generated/wspace-operations';\nimport { DashboardGlobalDataDocument } from '../../../generated/global-operations';\nimport type {\n WorkspaceDashboardData,\n GlobalDashboardData,\n VibeSummary,\n SessionSummary,\n AgentFleetSummary,\n} from '../types';\n\nconst BYTES_PER_GB = 1024 ** 3;\n\n// ─── Hooks ────────────────────────────────────────────────────────────────────\n\nexport function useProjectsDashboardData(authToken?: string | null) {\n const { workspaceId } = useActiveContext();\n\n return useQuery({\n queryKey: ['dashboard', 'projects', workspaceId],\n enabled: !!authToken && !!workspaceId,\n queryFn: async (): Promise<WorkspaceDashboardData['projects']> => {\n const result = await graphqlFetch<{\n projects: WorkspaceDashboardData['projects'];\n }>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query: print(DashboardProjectsDocument),\n variables: { filter: { workspaceId } },\n // Dashboard widgets degrade gracefully (empty state) when a subgraph\n // is unavailable; a transient backend failure here is not actionable\n // for the user, so suppress the global error toast/console log.\n suppressGlobalErrorEvent: true,\n });\n\n return result.data?.projects ?? { items: [], total: 0 };\n },\n staleTime: 2 * 60 * 1000,\n gcTime: 5 * 60 * 1000,\n retry: 1,\n refetchOnWindowFocus: true,\n });\n}\n\nexport function useWorkspaceDashboardData(authToken?: string | null) {\n const { workspaceId } = useActiveContext();\n\n return useQuery({\n queryKey: ['dashboard', 'workspace', workspaceId],\n enabled: !!authToken && !!workspaceId,\n queryFn: async (): Promise<WorkspaceDashboardData> => {\n // Run the dashboard's recent-activity fetch as its OWN request so a\n // single failed sibling subgraph (workspace resource usage, workflow\n // dashboardSummary, …) doesn't null the whole batched response and\n // turn the widget's `recentActivities` into []. The combined\n // DashboardWorkspaceData query still feeds every other widget.\n const [result, activitiesResult] = await Promise.all([\n graphqlFetch<{\n getRecentActivities: WorkspaceDashboardData['recentActivities'];\n upcomingEvents: WorkspaceDashboardData['upcomingEvents'];\n getTags: WorkspaceDashboardData['tags'];\n myGroups: {\n edges: Array<{ node: { id: string; name: string; description?: string } }>;\n totalCount: number;\n };\n dashboardSummary: WorkspaceDashboardData['workflowStats'];\n workspaceResourceUsage: { currentStorageGb: number } | null;\n workspaceResourceLimits: { maxStorageGb?: number | null } | null;\n }>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query: print(DashboardWorkspaceDataDocument),\n // This combined query fans out across several workspace subgraphs\n // (activity, calendar, tags, groups, workflow, files). A single\n // subgraph 500 nulls the whole response, but every field below is\n // null-coalesced to an empty state, so the dashboard still renders.\n // Suppress the global error toast/console log — a degraded subgraph\n // is not user-actionable here.\n suppressGlobalErrorEvent: true,\n }),\n graphqlFetch<{ getRecentActivities: WorkspaceDashboardData['recentActivities'] }>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query:\n 'query DashboardRecentActivities { getRecentActivities(limit: 8) { id action resourceType resourceId userId description details timestamp } }',\n suppressGlobalErrorEvent: true,\n }),\n ]);\n\n // Gracefully handle partial failures — the gateway may return data for some\n // subgraphs and errors for others due to individual services being down.\n const data = result.data;\n const storageUsage = data?.workspaceResourceUsage\n ? {\n usedBytes: Math.round(data.workspaceResourceUsage.currentStorageGb * BYTES_PER_GB),\n totalBytes: Math.round(\n (data.workspaceResourceLimits?.maxStorageGb ?? 0) * BYTES_PER_GB\n ),\n fileCount: null,\n }\n : null;\n\n return {\n // Workspace data comes from the global gateway (see useGlobalDashboardData)\n // and is merged in at the DashboardPage level.\n workspaces: { items: [], total: 0 },\n // Projects fetched separately via useProjectsDashboardData to avoid\n // being poisoned by other subgraph errors in the combined query.\n projects: { items: [], total: 0 },\n recentActivities: activitiesResult.data?.getRecentActivities ?? [],\n upcomingEvents: data?.upcomingEvents ?? [],\n tags: data?.getTags ?? { items: [], total: 0 },\n groups: {\n items: (data?.myGroups?.edges ?? []).map((e) => ({\n id: e.node.id,\n name: e.node.name,\n description: e.node.description,\n })),\n totalCount: data?.myGroups?.totalCount ?? 0,\n },\n workflowStats: data?.dashboardSummary ?? null,\n storageUsage,\n };\n },\n staleTime: 2 * 60 * 1000, // 2 minutes — dashboard data changes moderately\n gcTime: 5 * 60 * 1000,\n retry: 1,\n refetchOnWindowFocus: true,\n });\n}\n\n// ─── VibeControls-specific dashboard data ────────────────────────────────────\n// Issued as raw GraphQL strings to avoid coupling to codegen for the new\n// `recentVibes` + `agentFleetSummary` queries (added in wspace-vibecontrols-svc).\n// Falls back to empty state if any field is missing in the deployed schema,\n// so the dashboard degrades gracefully during schema rollout.\n\nconst VIBECONTROLS_DASHBOARD_QUERY = `\n query VibeControlsDashboardData {\n recentVibes(limit: 5) {\n id\n name\n status\n workspaceId\n updatedAt\n }\n activeSessions(pagination: { limit: 5 }) {\n items {\n id\n name\n status\n agent { id name }\n startedAt\n vibeId\n }\n }\n agentFleetSummary {\n total\n online\n offline\n degraded\n lastSeenAt\n }\n }\n`;\n\ninterface VibecontrolsDashboardResponse {\n recentVibes?: VibeSummary[] | null;\n activeSessions?: { items?: SessionSummary[] | null } | null;\n agentFleetSummary?: AgentFleetSummary | null;\n}\n\nexport interface VibecontrolsDashboardData {\n recentVibes: VibeSummary[];\n activeSessions: SessionSummary[];\n agentFleet: AgentFleetSummary | null;\n}\n\nexport function useVibecontrolsDashboardData(authToken?: string | null, enabled = false) {\n const { workspaceId } = useActiveContext();\n\n return useQuery({\n queryKey: ['dashboard', 'vibecontrols', workspaceId],\n enabled: !!authToken && !!workspaceId && enabled,\n queryFn: async (): Promise<VibecontrolsDashboardData> => {\n try {\n const result = await graphqlFetch<VibecontrolsDashboardResponse>({\n gateway: 'workspace',\n authToken: authToken || undefined,\n workspaceToken: true,\n query: VIBECONTROLS_DASHBOARD_QUERY,\n // Schema may not be deployed in this environment; handled locally\n // (empty fallback below). Suppress the global error toast/log.\n suppressGlobalErrorEvent: true,\n });\n const data = result.data;\n return {\n recentVibes: data?.recentVibes ?? [],\n activeSessions: data?.activeSessions?.items ?? [],\n agentFleet: data?.agentFleetSummary ?? null,\n };\n } catch {\n // Schema may not yet be deployed in this environment — render empty.\n return { recentVibes: [], activeSessions: [], agentFleet: null };\n }\n },\n staleTime: 60 * 1000,\n gcTime: 5 * 60 * 1000,\n retry: 0,\n refetchOnWindowFocus: true,\n });\n}\n\nexport function useGlobalDashboardData(authToken?: string | null) {\n return useQuery({\n queryKey: ['dashboard', 'global'],\n enabled: !!authToken,\n queryFn: async (): Promise<GlobalDashboardData> => {\n const result = await graphqlFetch<{\n currentUser: GlobalDashboardData['currentUser'];\n myDashboardPreferences: Record<string, unknown> | null;\n myWorkspaces: {\n items: Array<{\n id: string;\n name: string;\n status: string;\n type: string;\n createdAt: string;\n memberCount: number;\n }>;\n total: number;\n };\n myOrganizations: {\n items: Array<{\n id: string;\n name: string;\n status: string;\n type: string;\n createdAt: string;\n }>;\n total: number;\n };\n }>({\n gateway: 'global',\n authToken: authToken || undefined,\n query: print(DashboardGlobalDataDocument),\n // Dashboard data is null-coalesced to empty state below; a transient\n // subgraph failure should not raise a user-facing error here.\n suppressGlobalErrorEvent: true,\n });\n\n const data = result.data;\n\n return {\n currentUser: data?.currentUser ?? null,\n dashboardPreferences: data?.myDashboardPreferences\n ? (data.myDashboardPreferences as unknown as GlobalDashboardData['dashboardPreferences'])\n : null,\n workspaces: data?.myWorkspaces ?? { items: [], total: 0 },\n organizations: data?.myOrganizations ?? { items: [], total: 0 },\n };\n },\n staleTime: 5 * 60 * 1000, // 5 minutes — user info and prefs change rarely\n gcTime: 10 * 60 * 1000,\n retry: 1,\n });\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/pages/dashboard/reorder.ts
|
|
2
|
+
var e = 10;
|
|
3
|
+
function t(t) {
|
|
4
|
+
return t.map((t, n) => ({
|
|
5
|
+
...t,
|
|
6
|
+
order: (n + 1) * e
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
function n(e, n, r, i) {
|
|
10
|
+
if (n === r) return e;
|
|
11
|
+
let a = [...e].sort((e, t) => e.order - t.order), o = a.findIndex((e) => e.widgetId === n), s = a.findIndex((e) => e.widgetId === r);
|
|
12
|
+
if (o < 0 || s < 0) return e;
|
|
13
|
+
let c = a[o], l = a.filter((e, t) => t !== o), u = i === "above" ? l.findIndex((e) => e.widgetId === r) : l.findIndex((e) => e.widgetId === r) + 1;
|
|
14
|
+
return t([
|
|
15
|
+
...l.slice(0, u),
|
|
16
|
+
c,
|
|
17
|
+
...l.slice(u)
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
function r(e, t) {
|
|
21
|
+
return e < t.top + t.height / 2 ? "above" : "below";
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { r as dropPositionFromPointer, n as reorderByDropIndex };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=reorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reorder.js","names":[],"sources":["../../../src/pages/dashboard/reorder.ts"],"sourcesContent":["/**\n * Pure reorder helpers for the dashboard customizer drag-and-drop.\n *\n * Extracted from `DashboardCustomizer.tsx` so the ordering logic can be unit-\n * tested without DOM/React Test Renderer. The caller owns the live `WidgetConfig[]`\n * state and is responsible for cloning before mutating.\n *\n * Invariants:\n * - Input widgets are expected to already be sorted by `order` ascending. The\n * helper re-sorts defensively so callers don't have to.\n * - `enabled` and `size` are preserved on every widget; only `order` changes.\n * - Drop positions are evaluated relative to the row the pointer is over: the\n * upper half means \"insert before\", the lower half means \"insert after\".\n */\nimport type { WidgetConfig } from './types';\n\n/** Side of the target row the pointer is on, relative to the row midpoint. */\nexport type DropPosition = 'above' | 'below';\n\nconst ORDER_STEP = 10;\n\n/**\n * Re-number `order` so it is strictly increasing by `ORDER_STEP` from the\n * given array's order. Returns a fresh array; input is not mutated.\n */\nexport function renumberOrders(widgets: WidgetConfig[]): WidgetConfig[] {\n return widgets.map((w, idx) => ({ ...w, order: (idx + 1) * ORDER_STEP }));\n}\n\n/**\n * Move `fromId` to a position adjacent to `toId`.\n *\n * - `position === 'above'` — `fromId` ends up immediately before `toId`.\n * - `position === 'below'` — `fromId` ends up immediately after `toId`.\n *\n * If either id is missing, or `fromId === toId`, the input is returned\n * unchanged (referential equality preserved). Adjacent swaps are valid\n * moves — flicker prevention is the caller's responsibility (see\n * `DashboardCustomizer.handleDragOver`, which tracks the last-applied\n * move via a ref). [BOFF-7219]\n */\nexport function reorderByDropIndex(\n widgets: WidgetConfig[],\n fromId: string,\n toId: string,\n position: DropPosition\n): WidgetConfig[] {\n if (fromId === toId) return widgets;\n\n const sorted = [...widgets].sort((a, b) => a.order - b.order);\n const fromIdx = sorted.findIndex((w) => w.widgetId === fromId);\n const toIdx = sorted.findIndex((w) => w.widgetId === toId);\n if (fromIdx < 0 || toIdx < 0) return widgets;\n\n const moving = sorted[fromIdx];\n const without = sorted.filter((_, i) => i !== fromIdx);\n const insertIdx =\n position === 'above'\n ? without.findIndex((w) => w.widgetId === toId)\n : without.findIndex((w) => w.widgetId === toId) + 1;\n\n const reordered = [...without.slice(0, insertIdx), moving, ...without.slice(insertIdx)];\n return renumberOrders(reordered);\n}\n\n/**\n * Resolve the drop position from a pointer event and a target row's bounding\n * rect. `clientY` above the midpoint yields 'above', otherwise 'below'.\n */\nexport function dropPositionFromPointer(clientY: number, rect: DOMRect): DropPosition {\n return clientY < rect.top + rect.height / 2 ? 'above' : 'below';\n}\n"],"mappings":";AAmBA,IAAM,IAAa;AAMnB,SAAgB,EAAe,GAAyC;AACtE,QAAO,EAAQ,KAAK,GAAG,OAAS;EAAE,GAAG;EAAG,QAAQ,IAAM,KAAK;EAAY,EAAE;;AAe3E,SAAgB,EACd,GACA,GACA,GACA,GACgB;AAChB,KAAI,MAAW,EAAM,QAAO;CAE5B,IAAM,IAAS,CAAC,GAAG,EAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EACvD,IAAU,EAAO,WAAW,MAAM,EAAE,aAAa,EAAO,EACxD,IAAQ,EAAO,WAAW,MAAM,EAAE,aAAa,EAAK;AAC1D,KAAI,IAAU,KAAK,IAAQ,EAAG,QAAO;CAErC,IAAM,IAAS,EAAO,IAChB,IAAU,EAAO,QAAQ,GAAG,MAAM,MAAM,EAAQ,EAChD,IACJ,MAAa,UACT,EAAQ,WAAW,MAAM,EAAE,aAAa,EAAK,GAC7C,EAAQ,WAAW,MAAM,EAAE,aAAa,EAAK,GAAG;AAGtD,QAAO,EADW;EAAC,GAAG,EAAQ,MAAM,GAAG,EAAU;EAAE;EAAQ,GAAG,EAAQ,MAAM,EAAU;EAAC,CACvD;;AAOlC,SAAgB,EAAwB,GAAiB,GAA6B;AACpF,QAAO,IAAU,EAAK,MAAM,EAAK,SAAS,IAAI,UAAU"}
|