@locusai/web 0.1.7 → 0.2.2
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/CHANGELOG.md +26 -0
- package/next.config.js +15 -2
- package/package.json +26 -3
- package/src/app/(auth)/invite/page.tsx +109 -0
- package/src/app/(auth)/layout.tsx +19 -0
- package/src/app/(auth)/login/page.tsx +65 -0
- package/src/app/(auth)/onboarding/workspace/page.tsx +46 -0
- package/src/app/(auth)/register/page.tsx +165 -0
- package/src/app/(dashboard)/activity/page.tsx +7 -0
- package/src/app/(dashboard)/backlog/page.tsx +195 -0
- package/src/app/(dashboard)/board/page.tsx +141 -0
- package/src/app/(dashboard)/layout.tsx +32 -0
- package/src/app/(dashboard)/page.tsx +14 -0
- package/src/app/(dashboard)/settings/page.tsx +161 -0
- package/src/app/(dashboard)/settings/team/page.tsx +75 -0
- package/src/app/globals.css +259 -0
- package/src/app/layout.tsx +10 -20
- package/src/app/providers.tsx +26 -3
- package/src/components/AuthLayoutUI.tsx +53 -0
- package/src/components/BoardFilter.tsx +75 -74
- package/src/components/CreateModal/CreateModal.tsx +142 -0
- package/src/components/CreateModal/index.ts +1 -0
- package/src/components/Editor.tsx +279 -0
- package/src/components/Header.tsx +99 -12
- package/src/components/PageLayout.tsx +69 -0
- package/src/components/PropertyItem.tsx +55 -9
- package/src/components/Sidebar.tsx +280 -36
- package/src/components/SprintCreateModal.tsx +84 -0
- package/src/components/TaskCard.tsx +196 -78
- package/src/components/TaskCreateModal.tsx +181 -178
- package/src/components/TaskPanel.tsx +140 -692
- package/src/components/WorkspaceCreateModal.tsx +97 -0
- package/src/components/WorkspaceProtected.tsx +91 -0
- package/src/components/auth/InviteSteps.tsx +220 -0
- package/src/components/auth/LoginSteps.tsx +86 -0
- package/src/components/auth/RegisterSteps.tsx +371 -0
- package/src/components/auth/index.ts +3 -0
- package/src/components/backlog/BacklogList.tsx +92 -0
- package/src/components/backlog/BacklogSection.tsx +137 -0
- package/src/components/backlog/CompletedSprintItem.tsx +95 -0
- package/src/components/backlog/CompletedSprintsSection.tsx +77 -0
- package/src/components/backlog/SprintSection.tsx +155 -0
- package/src/components/backlog/constants.ts +26 -0
- package/src/components/board/BoardColumn.tsx +97 -0
- package/src/components/board/BoardContent.tsx +84 -0
- package/src/components/board/BoardEmptyState.tsx +82 -0
- package/src/components/board/BoardHeader.tsx +47 -0
- package/src/components/board/SprintMindmap.tsx +65 -0
- package/src/components/board/constants.ts +40 -0
- package/src/components/board/index.ts +5 -0
- package/src/components/common/ErrorState.tsx +124 -0
- package/src/components/common/LoadingState.tsx +83 -0
- package/src/components/common/index.ts +40 -0
- package/src/components/dashboard/ActivityFeed.tsx +77 -0
- package/src/components/dashboard/ActivityItem.tsx +207 -0
- package/src/components/dashboard/QuickActions.tsx +50 -0
- package/src/components/dashboard/StatCard.tsx +79 -0
- package/src/components/dnd/index.tsx +51 -0
- package/src/components/docs/DocsEditorArea.tsx +87 -0
- package/src/components/docs/DocsHeaderActions.tsx +121 -0
- package/src/components/docs/DocsSidebar.tsx +351 -0
- package/src/components/index.ts +7 -0
- package/src/components/onboarding/StepProgress.tsx +21 -0
- package/src/components/onboarding/index.ts +1 -0
- package/src/components/settings/ApiKeyConfirmationModal.tsx +81 -0
- package/src/components/settings/ApiKeyCreatedModal.tsx +96 -0
- package/src/components/settings/ApiKeysList.tsx +143 -0
- package/src/components/settings/ApiKeysSettings.tsx +144 -0
- package/src/components/settings/InviteMemberModal.tsx +106 -0
- package/src/components/settings/ProjectSetupGuide.tsx +147 -0
- package/src/components/settings/SettingItem.tsx +32 -0
- package/src/components/settings/SettingSection.tsx +50 -0
- package/src/components/settings/TeamInvitationsList.tsx +90 -0
- package/src/components/settings/TeamMembersList.tsx +95 -0
- package/src/components/settings/index.ts +8 -0
- package/src/components/task-panel/TaskActivity.tsx +127 -0
- package/src/components/task-panel/TaskChecklist.tsx +142 -0
- package/src/components/task-panel/TaskDescription.tsx +201 -0
- package/src/components/task-panel/TaskDocs.tsx +137 -0
- package/src/components/task-panel/TaskHeader.tsx +125 -0
- package/src/components/task-panel/TaskProperties.tsx +111 -0
- package/src/components/task-panel/index.ts +12 -0
- package/src/components/typography/EmptyStateText.tsx +59 -0
- package/src/components/typography/MetadataText.tsx +65 -0
- package/src/components/typography/SecondaryText.tsx +60 -0
- package/src/components/typography/SectionLabel.tsx +60 -0
- package/src/components/typography/index.ts +14 -0
- package/src/components/typography-scales.tsx +218 -0
- package/src/components/ui/Avatar.tsx +123 -0
- package/src/components/ui/Badge.tsx +69 -2
- package/src/components/ui/Button.tsx +71 -30
- package/src/components/ui/Checkbox.tsx +34 -0
- package/src/components/ui/Dropdown.tsx +67 -1
- package/src/components/ui/EmptyState.tsx +129 -0
- package/src/components/ui/Input.tsx +53 -6
- package/src/components/ui/Modal.tsx +45 -12
- package/src/components/ui/OtpInput.tsx +148 -0
- package/src/components/ui/Skeleton.tsx +36 -0
- package/src/components/ui/Spinner.tsx +112 -0
- package/src/components/ui/Textarea.tsx +28 -3
- package/src/components/ui/Toast.tsx +99 -0
- package/src/components/ui/Toggle.tsx +63 -0
- package/src/components/ui/constants.ts +108 -0
- package/src/components/ui/index.ts +7 -0
- package/src/context/AuthContext.tsx +140 -0
- package/src/context/index.ts +1 -0
- package/src/hooks/backlog/index.ts +13 -0
- package/src/hooks/backlog/useBacklogActions.ts +144 -0
- package/src/hooks/backlog/useBacklogComposite.ts +73 -0
- package/src/hooks/backlog/useBacklogData.ts +74 -0
- package/src/hooks/backlog/useBacklogDragDrop.ts +118 -0
- package/src/hooks/backlog/useBacklogUI.ts +74 -0
- package/src/hooks/index.ts +22 -0
- package/src/hooks/task-panel/index.ts +13 -0
- package/src/hooks/task-panel/useTaskActions.ts +200 -0
- package/src/hooks/task-panel/useTaskComputedValues.ts +30 -0
- package/src/hooks/task-panel/useTaskData.ts +78 -0
- package/src/hooks/task-panel/useTaskPanelComposite.ts +161 -0
- package/src/hooks/task-panel/useTaskUIState.ts +80 -0
- package/src/hooks/useAuthLayoutLogic.ts +43 -0
- package/src/hooks/useAuthenticatedUser.ts +36 -0
- package/src/hooks/useAuthenticatedUserWithOrg.ts +41 -0
- package/src/hooks/useBacklog.ts +303 -0
- package/src/hooks/useBoard.ts +230 -0
- package/src/hooks/useDashboardLayout.ts +49 -0
- package/src/hooks/useDocs.ts +279 -0
- package/src/hooks/useDocsQuery.ts +99 -0
- package/src/hooks/useDocsSidebarState.ts +104 -0
- package/src/hooks/useFormState.ts +40 -0
- package/src/hooks/useGlobalKeydowns.ts +52 -0
- package/src/hooks/useInviteForm.ts +122 -0
- package/src/hooks/useLoginForm.ts +84 -0
- package/src/hooks/useMutationWithToast.ts +56 -0
- package/src/hooks/useOrganizationQuery.ts +55 -0
- package/src/hooks/useRegisterForm.ts +216 -0
- package/src/hooks/useSprintsQuery.ts +38 -0
- package/src/hooks/useTaskDescription.ts +102 -0
- package/src/hooks/useTaskPanel.ts +341 -0
- package/src/hooks/useTasksQuery.ts +39 -0
- package/src/hooks/useTeamManagement.ts +92 -0
- package/src/hooks/useWorkspaceCreateForm.ts +70 -0
- package/src/hooks/useWorkspaceId.ts +29 -0
- package/src/lib/api-client.ts +40 -23
- package/src/lib/config.ts +25 -0
- package/src/lib/constants.ts +83 -0
- package/src/lib/options.ts +96 -0
- package/src/lib/query-keys.ts +91 -0
- package/src/lib/typography.ts +103 -0
- package/src/lib/utils.ts +4 -0
- package/src/lib/validation.ts +192 -0
- package/src/services/index.ts +7 -3
- package/src/services/notifications.ts +80 -0
- package/src/utils/env.utils.ts +15 -0
- package/src/views/ActivityView.tsx +123 -0
- package/src/views/Dashboard.tsx +126 -0
- package/src/views/Docs.tsx +98 -612
- package/tsconfig.tsbuildinfo +1 -1
- package/.next/BUILD_ID +0 -1
- package/.next/app-build-manifest.json +0 -55
- package/.next/app-path-routes-manifest.json +0 -8
- package/.next/build-manifest.json +0 -33
- package/.next/cache/.previewinfo +0 -1
- package/.next/cache/.rscinfo +0 -1
- package/.next/cache/.tsbuildinfo +0 -1
- package/.next/cache/config.json +0 -7
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/diagnostics/build-diagnostics.json +0 -6
- package/.next/diagnostics/framework.json +0 -1
- package/.next/export-detail.json +0 -5
- package/.next/export-marker.json +0 -6
- package/.next/images-manifest.json +0 -57
- package/.next/next-minimal-server.js.nft.json +0 -1
- package/.next/next-server.js.nft.json +0 -1
- package/.next/package.json +0 -1
- package/.next/prerender-manifest.json +0 -137
- package/.next/react-loadable-manifest.json +0 -32
- package/.next/required-server-files.json +0 -324
- package/.next/routes-manifest.json +0 -77
- package/.next/server/app/_not-found/page.js +0 -2
- package/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -1
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -8
- package/.next/server/app/_not-found.rsc +0 -21
- package/.next/server/app/backlog/page.js +0 -2
- package/.next/server/app/backlog/page.js.nft.json +0 -1
- package/.next/server/app/backlog/page_client-reference-manifest.js +0 -1
- package/.next/server/app/backlog.html +0 -1
- package/.next/server/app/backlog.meta +0 -7
- package/.next/server/app/backlog.rsc +0 -25
- package/.next/server/app/docs/page.js +0 -97
- package/.next/server/app/docs/page.js.nft.json +0 -1
- package/.next/server/app/docs/page_client-reference-manifest.js +0 -1
- package/.next/server/app/docs.html +0 -1
- package/.next/server/app/docs.meta +0 -7
- package/.next/server/app/docs.rsc +0 -26
- package/.next/server/app/favicon.ico/route.js +0 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +0 -1
- package/.next/server/app/index.html +0 -1
- package/.next/server/app/index.meta +0 -7
- package/.next/server/app/index.rsc +0 -25
- package/.next/server/app/page.js +0 -2
- package/.next/server/app/page.js.nft.json +0 -1
- package/.next/server/app/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings/page.js +0 -2
- package/.next/server/app/settings/page.js.nft.json +0 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +0 -1
- package/.next/server/app/settings.html +0 -1
- package/.next/server/app/settings.meta +0 -7
- package/.next/server/app/settings.rsc +0 -25
- package/.next/server/app-paths-manifest.json +0 -8
- package/.next/server/chunks/496.js +0 -6
- package/.next/server/chunks/585.js +0 -1
- package/.next/server/chunks/665.js +0 -1
- package/.next/server/chunks/699.js +0 -22
- package/.next/server/chunks/852.js +0 -7
- package/.next/server/functions-config-manifest.json +0 -4
- package/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/.next/server/middleware-build-manifest.js +0 -1
- package/.next/server/middleware-manifest.json +0 -6
- package/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/.next/server/next-font-manifest.js +0 -1
- package/.next/server/next-font-manifest.json +0 -1
- package/.next/server/pages/404.html +0 -1
- package/.next/server/pages/500.html +0 -1
- package/.next/server/pages/_app.js +0 -1
- package/.next/server/pages/_app.js.nft.json +0 -1
- package/.next/server/pages/_document.js +0 -1
- package/.next/server/pages/_document.js.nft.json +0 -1
- package/.next/server/pages/_error.js +0 -19
- package/.next/server/pages/_error.js.nft.json +0 -1
- package/.next/server/pages-manifest.json +0 -6
- package/.next/server/server-reference-manifest.js +0 -1
- package/.next/server/server-reference-manifest.json +0 -1
- package/.next/server/webpack-runtime.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/.next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/.next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/.next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/.next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/.next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/.next/static/chunks/487-1808785ba665f784.js +0 -1
- package/.next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/.next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/.next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/.next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/.next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/.next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/.next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/.next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/.next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/.next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/.next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/.next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/.next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/.next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/.next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/.next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/.next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/.next/static/css/13e8617b72f9d3aa.css +0 -1
- package/.next/static/css/8aea088cdc4338f0.css +0 -1
- package/.next/static/css/b301ab0424111664.css +0 -1
- package/.next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/.next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/.next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/.next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/.next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/.next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/.next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/.next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/.next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/.next/trace +0 -46
- package/.next/types/app/backlog/page.ts +0 -84
- package/.next/types/app/docs/page.ts +0 -84
- package/.next/types/app/layout.ts +0 -84
- package/.next/types/app/page.ts +0 -84
- package/.next/types/app/settings/page.ts +0 -84
- package/.next/types/cache-life.d.ts +0 -141
- package/.next/types/package.json +0 -1
- package/next-env.d.ts +0 -5
- package/out/404.html +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_buildManifest.js +0 -1
- package/out/_next/static/D0NXe04ZCLNDckV_quc8g/_ssgManifest.js +0 -1
- package/out/_next/static/chunks/138.b98511c56423f8bb.js +0 -1
- package/out/_next/static/chunks/146-34259952c594a3b0.js +0 -1
- package/out/_next/static/chunks/337-d3bb75304d130513.js +0 -1
- package/out/_next/static/chunks/477.1a6ecfe53375bd9c.js +0 -1
- package/out/_next/static/chunks/487-1808785ba665f784.js +0 -1
- package/out/_next/static/chunks/544.a9569941cc886e9d.js +0 -1
- package/out/_next/static/chunks/87c73c54-1f4741035a95c140.js +0 -1
- package/out/_next/static/chunks/902-d6926825a9fe8784.js +0 -1
- package/out/_next/static/chunks/955-c8f8f6235ae8f8c6.js +0 -1
- package/out/_next/static/chunks/996.e0a334e6ae90900e.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-44b1804abb44a34d.js +0 -1
- package/out/_next/static/chunks/app/backlog/page-dce1450769bfae8f.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1efee819f25492cb.js +0 -1
- package/out/_next/static/chunks/app/layout-05f504c042b9f7ee.js +0 -1
- package/out/_next/static/chunks/app/page-3fd91aaaa4776ced.js +0 -1
- package/out/_next/static/chunks/app/settings/page-84e16c9638d657e4.js +0 -1
- package/out/_next/static/chunks/framework-152a1bc8c81c7458.js +0 -1
- package/out/_next/static/chunks/main-843ab130fc1be309.js +0 -1
- package/out/_next/static/chunks/main-app-123e879c5a937a00.js +0 -1
- package/out/_next/static/chunks/pages/_app-a050a8e6e4fb04cf.js +0 -1
- package/out/_next/static/chunks/pages/_error-3e422ffd891594de.js +0 -1
- package/out/_next/static/chunks/polyfills-42372ed130431b0a.js +0 -1
- package/out/_next/static/chunks/webpack-99a10a055b5bb9c4.js +0 -1
- package/out/_next/static/css/13e8617b72f9d3aa.css +0 -1
- package/out/_next/static/css/8aea088cdc4338f0.css +0 -1
- package/out/_next/static/css/b301ab0424111664.css +0 -1
- package/out/_next/static/media/24c15609eaa28576-s.woff2 +0 -0
- package/out/_next/static/media/2c07349e02a7b712-s.woff2 +0 -0
- package/out/_next/static/media/456105d6ea6d39e0-s.woff2 +0 -0
- package/out/_next/static/media/47cbc4e2adbc5db9-s.p.woff2 +0 -0
- package/out/_next/static/media/4f77bef990aad698-s.woff2 +0 -0
- package/out/_next/static/media/627d916fd739a539-s.woff2 +0 -0
- package/out/_next/static/media/63b255f18bea0ca9-s.woff2 +0 -0
- package/out/_next/static/media/70bd82ac89b4fa42-s.woff2 +0 -0
- package/out/_next/static/media/84602850c8fd81c3-s.woff2 +0 -0
- package/out/backlog.html +0 -1
- package/out/backlog.txt +0 -25
- package/out/docs.html +0 -1
- package/out/docs.txt +0 -26
- package/out/favicon.ico +0 -0
- package/out/index.html +0 -1
- package/out/index.txt +0 -25
- package/out/logo.png +0 -0
- package/out/settings.html +0 -1
- package/out/settings.txt +0 -25
- package/src/app/backlog/page.tsx +0 -19
- package/src/app/page.tsx +0 -16
- package/src/app/settings/page.tsx +0 -194
- package/src/hooks/useTasks.ts +0 -119
- package/src/services/doc.service.ts +0 -27
- package/src/services/sprint.service.ts +0 -24
- package/src/services/task.service.ts +0 -75
- package/src/views/Backlog.tsx +0 -691
- package/src/views/Board.tsx +0 -306
- /package/src/app/{docs → (dashboard)/docs}/page.tsx +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type User } from "@locusai/shared";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
import { useAuth } from "@/context/AuthContext";
|
|
7
|
+
import { isCloudMode } from "@/utils/env.utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook for pages that require an authenticated user.
|
|
11
|
+
* Returns a guaranteed non-null User object.
|
|
12
|
+
* Redirects to login if user is not authenticated.
|
|
13
|
+
*
|
|
14
|
+
* Usage: Only use this in pages/components that are already protected
|
|
15
|
+
* by authentication middleware (wrapped with auth guards).
|
|
16
|
+
*
|
|
17
|
+
* @returns {User} The authenticated user (guaranteed non-null)
|
|
18
|
+
* @throws Redirects to login if not authenticated
|
|
19
|
+
*/
|
|
20
|
+
export function useAuthenticatedUser(): User {
|
|
21
|
+
const { user, isLoading, isAuthenticated } = useAuth();
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Redirect to login if not authenticated (only in cloud mode)
|
|
26
|
+
if (!isLoading && !isAuthenticated && isCloudMode()) {
|
|
27
|
+
router.push("/login");
|
|
28
|
+
}
|
|
29
|
+
}, [isLoading, isAuthenticated, router]);
|
|
30
|
+
|
|
31
|
+
// Type assertion is safe here because:
|
|
32
|
+
// 1. This hook is only used in authenticated contexts
|
|
33
|
+
// 2. If user is null, we redirect to login above
|
|
34
|
+
// 3. The caller is responsible for ensuring this component is protected
|
|
35
|
+
return user as User;
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type User } from "@locusai/shared";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
import { useAuth } from "@/context/AuthContext";
|
|
7
|
+
import { isCloudMode } from "@/utils/env.utils";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hook for pages that require an authenticated user WITH an organization.
|
|
11
|
+
* Returns a guaranteed non-null User object with a non-null orgId.
|
|
12
|
+
* Redirects to login if user is not authenticated or has no organization.
|
|
13
|
+
*
|
|
14
|
+
* Usage: Only use this in pages/components that are already protected
|
|
15
|
+
* by authentication middleware and require organization context.
|
|
16
|
+
*
|
|
17
|
+
* @returns {User} The authenticated user with orgId (both guaranteed non-null)
|
|
18
|
+
* @throws Redirects to login if not authenticated or no organization
|
|
19
|
+
*/
|
|
20
|
+
export function useAuthenticatedUserWithOrg(): User & { orgId: string } {
|
|
21
|
+
const { user, isLoading, isAuthenticated } = useAuth();
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
// Redirect to login if not authenticated (only in cloud mode)
|
|
26
|
+
if (!isLoading && !isAuthenticated && isCloudMode()) {
|
|
27
|
+
router.push("/login");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Redirect to onboarding if no organization
|
|
31
|
+
if (!isLoading && isAuthenticated && user && !user.orgId && isCloudMode()) {
|
|
32
|
+
router.push("/onboarding/workspace");
|
|
33
|
+
}
|
|
34
|
+
}, [isLoading, isAuthenticated, user, router]);
|
|
35
|
+
|
|
36
|
+
// Type assertion is safe here because:
|
|
37
|
+
// 1. This hook is only used in authenticated contexts with org
|
|
38
|
+
// 2. If user is null or has no orgId, we redirect above
|
|
39
|
+
// 3. The caller is responsible for ensuring this component is protected
|
|
40
|
+
return user as User & { orgId: string };
|
|
41
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DragEndEvent,
|
|
5
|
+
type DragStartEvent,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
} from "@dnd-kit/core";
|
|
10
|
+
import { type Sprint, SprintStatus, type Task } from "@locusai/shared";
|
|
11
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
13
|
+
import { useEffect, useMemo, useState } from "react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { useSprintsQuery, useTasksQuery } from "@/hooks";
|
|
16
|
+
import { useWorkspaceId } from "@/hooks/useWorkspaceId";
|
|
17
|
+
import { locusClient } from "@/lib/api-client";
|
|
18
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
19
|
+
|
|
20
|
+
export function useBacklog() {
|
|
21
|
+
const workspaceId = useWorkspaceId();
|
|
22
|
+
const queryClient = useQueryClient();
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const searchParams = useSearchParams();
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
data: tasks = [],
|
|
28
|
+
isLoading: tasksLoading,
|
|
29
|
+
refetch: refetchTasks,
|
|
30
|
+
} = useTasksQuery();
|
|
31
|
+
const {
|
|
32
|
+
data: sprints = [],
|
|
33
|
+
isLoading: sprintsLoading,
|
|
34
|
+
refetch: refetchSprints,
|
|
35
|
+
} = useSprintsQuery();
|
|
36
|
+
|
|
37
|
+
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
|
|
38
|
+
const [isSprintModalOpen, setIsSprintModalOpen] = useState(false);
|
|
39
|
+
const [selectedTaskId, setSelectedTaskIdState] = useState<string | null>(
|
|
40
|
+
null
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Sync URL query param with state on mount
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const taskIdFromUrl = searchParams.get("taskId");
|
|
46
|
+
if (taskIdFromUrl) {
|
|
47
|
+
setSelectedTaskIdState(taskIdFromUrl);
|
|
48
|
+
}
|
|
49
|
+
}, [searchParams]);
|
|
50
|
+
|
|
51
|
+
const setSelectedTaskId = (id: string | null) => {
|
|
52
|
+
setSelectedTaskIdState(id);
|
|
53
|
+
if (id) {
|
|
54
|
+
router.push(`/backlog?taskId=${id}`, { scroll: false });
|
|
55
|
+
} else {
|
|
56
|
+
router.push("/backlog", { scroll: false });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
60
|
+
const [expandedSections, setExpandedSections] = useState<Set<string>>(
|
|
61
|
+
new Set(["active", "planned"])
|
|
62
|
+
);
|
|
63
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
64
|
+
|
|
65
|
+
// Handle query parameters for new task/sprint
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const createTask = searchParams.get("createTask");
|
|
68
|
+
const createSprint = searchParams.get("createSprint");
|
|
69
|
+
|
|
70
|
+
if (createTask === "true") {
|
|
71
|
+
setIsTaskModalOpen(true);
|
|
72
|
+
router.replace("/backlog");
|
|
73
|
+
} else if (createSprint === "true") {
|
|
74
|
+
setIsSprintModalOpen(true);
|
|
75
|
+
router.replace("/backlog");
|
|
76
|
+
}
|
|
77
|
+
}, [searchParams, router]);
|
|
78
|
+
|
|
79
|
+
// Sensors for drag and drop
|
|
80
|
+
const sensors = useSensors(
|
|
81
|
+
useSensor(PointerSensor, {
|
|
82
|
+
activationConstraint: { distance: 8 },
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Group tasks efficiently
|
|
87
|
+
const { backlogTasks, activeSprint, plannedSprints, completedSprints } =
|
|
88
|
+
useMemo(() => {
|
|
89
|
+
return {
|
|
90
|
+
backlogTasks: tasks.filter((t) => !t.sprintId),
|
|
91
|
+
activeSprint: sprints.find((s) => s.status === SprintStatus.ACTIVE),
|
|
92
|
+
plannedSprints: sprints.filter(
|
|
93
|
+
(s) => s.status === SprintStatus.PLANNED
|
|
94
|
+
),
|
|
95
|
+
completedSprints: sprints.filter(
|
|
96
|
+
(s) => s.status === SprintStatus.COMPLETED
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}, [tasks, sprints]);
|
|
100
|
+
|
|
101
|
+
const getSprintTasks = (sprintId: string) =>
|
|
102
|
+
tasks.filter((t) => t.sprintId === sprintId);
|
|
103
|
+
|
|
104
|
+
const toggleSection = (section: string) => {
|
|
105
|
+
const newExpanded = new Set(expandedSections);
|
|
106
|
+
if (newExpanded.has(section)) {
|
|
107
|
+
newExpanded.delete(section);
|
|
108
|
+
} else {
|
|
109
|
+
newExpanded.add(section);
|
|
110
|
+
}
|
|
111
|
+
setExpandedSections(newExpanded);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Drag handlers
|
|
115
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
116
|
+
const task = tasks.find((t) => t.id === event.active.id);
|
|
117
|
+
setActiveTask(task || null);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const handleDragEnd = async (event: DragEndEvent) => {
|
|
121
|
+
const { active, over } = event;
|
|
122
|
+
setActiveTask(null);
|
|
123
|
+
|
|
124
|
+
if (!over) return;
|
|
125
|
+
|
|
126
|
+
const taskId = active.id as string;
|
|
127
|
+
const overId = over.id as string;
|
|
128
|
+
|
|
129
|
+
// Find the actual target container (sprint ID or "backlog")
|
|
130
|
+
let targetContainerId = overId;
|
|
131
|
+
const overTask = tasks.find((t) => t.id === overId);
|
|
132
|
+
if (overTask) {
|
|
133
|
+
targetContainerId = overTask.sprintId
|
|
134
|
+
? `sprint-${overTask.sprintId}`
|
|
135
|
+
: "backlog";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let newSprintId: string | null = null;
|
|
139
|
+
if (targetContainerId === "backlog") {
|
|
140
|
+
newSprintId = null;
|
|
141
|
+
} else if (targetContainerId.startsWith("sprint-")) {
|
|
142
|
+
newSprintId = targetContainerId.replace("sprint-", "");
|
|
143
|
+
} else {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
148
|
+
if (!task || task.sprintId === newSprintId) return;
|
|
149
|
+
|
|
150
|
+
// Optimistic update
|
|
151
|
+
const queryKey = queryKeys.tasks.list(workspaceId);
|
|
152
|
+
await queryClient.cancelQueries({ queryKey });
|
|
153
|
+
|
|
154
|
+
const previousTasks = queryClient.getQueryData<Task[]>(queryKey);
|
|
155
|
+
|
|
156
|
+
if (previousTasks) {
|
|
157
|
+
queryClient.setQueryData<Task[]>(
|
|
158
|
+
queryKey,
|
|
159
|
+
previousTasks.map((t) =>
|
|
160
|
+
t.id === taskId ? { ...t, sprintId: newSprintId } : t
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await locusClient.tasks.update(taskId, workspaceId, {
|
|
167
|
+
sprintId: newSprintId,
|
|
168
|
+
});
|
|
169
|
+
// Invalidate to ensure sync
|
|
170
|
+
queryClient.invalidateQueries({ queryKey });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// Rollback
|
|
173
|
+
if (previousTasks) {
|
|
174
|
+
queryClient.setQueryData(queryKey, previousTasks);
|
|
175
|
+
}
|
|
176
|
+
toast.error(
|
|
177
|
+
error instanceof Error ? error.message : "Failed to move task"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Sprint actions
|
|
183
|
+
const handleCreateSprint = async (name: string) => {
|
|
184
|
+
try {
|
|
185
|
+
setIsSubmitting(true);
|
|
186
|
+
await locusClient.sprints.create(workspaceId, { name });
|
|
187
|
+
toast.success("Sprint created");
|
|
188
|
+
setIsSprintModalOpen(false);
|
|
189
|
+
refetchSprints();
|
|
190
|
+
queryClient.invalidateQueries({
|
|
191
|
+
queryKey: queryKeys.sprints.list(workspaceId),
|
|
192
|
+
});
|
|
193
|
+
} catch (error) {
|
|
194
|
+
toast.error(
|
|
195
|
+
error instanceof Error ? error.message : "Failed to create sprint"
|
|
196
|
+
);
|
|
197
|
+
} finally {
|
|
198
|
+
setIsSubmitting(false);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleStartSprint = async (sprintId: string) => {
|
|
203
|
+
try {
|
|
204
|
+
setIsSubmitting(true);
|
|
205
|
+
await locusClient.sprints.start(sprintId, workspaceId);
|
|
206
|
+
toast.success("Sprint started");
|
|
207
|
+
refetchSprints();
|
|
208
|
+
queryClient.invalidateQueries({
|
|
209
|
+
queryKey: queryKeys.sprints.list(workspaceId),
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
toast.error(
|
|
213
|
+
error instanceof Error ? error.message : "Failed to start sprint"
|
|
214
|
+
);
|
|
215
|
+
} finally {
|
|
216
|
+
setIsSubmitting(false);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const handleCompleteSprint = async (sprintId: string) => {
|
|
221
|
+
try {
|
|
222
|
+
setIsSubmitting(true);
|
|
223
|
+
|
|
224
|
+
// Optimistic update
|
|
225
|
+
const sprintKey = queryKeys.sprints.list(workspaceId);
|
|
226
|
+
const previousSprints = queryClient.getQueryData<Sprint[]>(sprintKey);
|
|
227
|
+
|
|
228
|
+
if (previousSprints) {
|
|
229
|
+
queryClient.setQueryData<Sprint[]>(
|
|
230
|
+
sprintKey,
|
|
231
|
+
previousSprints.map((s) =>
|
|
232
|
+
s.id === sprintId
|
|
233
|
+
? {
|
|
234
|
+
...s,
|
|
235
|
+
status: SprintStatus.COMPLETED,
|
|
236
|
+
endDate: Date.now(),
|
|
237
|
+
}
|
|
238
|
+
: s
|
|
239
|
+
)
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await locusClient.sprints.complete(sprintId, workspaceId);
|
|
244
|
+
toast.success("Sprint completed");
|
|
245
|
+
|
|
246
|
+
// Full sync
|
|
247
|
+
queryClient.invalidateQueries({ queryKey: sprintKey });
|
|
248
|
+
queryClient.invalidateQueries({
|
|
249
|
+
queryKey: queryKeys.tasks.list(workspaceId),
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
toast.error(
|
|
253
|
+
error instanceof Error ? error.message : "Failed to complete sprint"
|
|
254
|
+
);
|
|
255
|
+
} finally {
|
|
256
|
+
setIsSubmitting(false);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const handleDeleteTask = async (taskId: string) => {
|
|
261
|
+
try {
|
|
262
|
+
await locusClient.tasks.delete(taskId, workspaceId);
|
|
263
|
+
toast.success("Task deleted");
|
|
264
|
+
refetchTasks();
|
|
265
|
+
} catch (error) {
|
|
266
|
+
toast.error(
|
|
267
|
+
error instanceof Error ? error.message : "Failed to delete task"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const isLoading = tasksLoading || sprintsLoading;
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
tasks,
|
|
276
|
+
sprints,
|
|
277
|
+
backlogTasks,
|
|
278
|
+
activeSprint,
|
|
279
|
+
plannedSprints,
|
|
280
|
+
completedSprints,
|
|
281
|
+
getSprintTasks,
|
|
282
|
+
isLoading,
|
|
283
|
+
isTaskModalOpen,
|
|
284
|
+
setIsTaskModalOpen,
|
|
285
|
+
isSprintModalOpen,
|
|
286
|
+
setIsSprintModalOpen,
|
|
287
|
+
selectedTaskId,
|
|
288
|
+
setSelectedTaskId,
|
|
289
|
+
activeTask,
|
|
290
|
+
expandedSections,
|
|
291
|
+
isSubmitting,
|
|
292
|
+
sensors,
|
|
293
|
+
toggleSection,
|
|
294
|
+
handleDragStart,
|
|
295
|
+
handleDragEnd,
|
|
296
|
+
handleCreateSprint,
|
|
297
|
+
handleStartSprint,
|
|
298
|
+
handleCompleteSprint,
|
|
299
|
+
handleDeleteTask,
|
|
300
|
+
refetchTasks,
|
|
301
|
+
refetchSprints,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type DragEndEvent,
|
|
5
|
+
type DragStartEvent,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
} from "@dnd-kit/core";
|
|
10
|
+
import { SprintStatus, type Task, TaskStatus } from "@locusai/shared";
|
|
11
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
12
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
13
|
+
import { useEffect, useMemo, useState } from "react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { BOARD_STATUSES } from "@/components/board/constants";
|
|
16
|
+
import { useSprintsQuery, useTasksQuery } from "@/hooks";
|
|
17
|
+
import { useWorkspaceId } from "@/hooks/useWorkspaceId";
|
|
18
|
+
import { locusClient } from "@/lib/api-client";
|
|
19
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
20
|
+
|
|
21
|
+
export function useBoard() {
|
|
22
|
+
const workspaceId = useWorkspaceId();
|
|
23
|
+
const queryClient = useQueryClient();
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const searchParams = useSearchParams();
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
data: tasks = [],
|
|
29
|
+
isLoading: tasksLoading,
|
|
30
|
+
refetch,
|
|
31
|
+
} = useTasksQuery();
|
|
32
|
+
const { data: sprints = [], isLoading: sprintsLoading } = useSprintsQuery();
|
|
33
|
+
|
|
34
|
+
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
35
|
+
const [selectedTaskId, setSelectedTaskIdState] = useState<string | null>(
|
|
36
|
+
null
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Sync URL query param with state on mount
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const taskIdFromUrl = searchParams.get("taskId");
|
|
42
|
+
if (taskIdFromUrl) {
|
|
43
|
+
setSelectedTaskIdState(taskIdFromUrl);
|
|
44
|
+
}
|
|
45
|
+
}, [searchParams]);
|
|
46
|
+
|
|
47
|
+
const setSelectedTaskId = (id: string | null) => {
|
|
48
|
+
setSelectedTaskIdState(id);
|
|
49
|
+
if (id) {
|
|
50
|
+
router.push(`/board?taskId=${id}`, { scroll: false });
|
|
51
|
+
} else {
|
|
52
|
+
router.push("/board", { scroll: false });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
56
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
57
|
+
const [priorityFilter, setPriorityFilter] = useState<string | null>(null);
|
|
58
|
+
const [roleFilter, setRoleFilter] = useState<string | null>(null);
|
|
59
|
+
const [view, setView] = useState<"board" | "mindmap">("board");
|
|
60
|
+
|
|
61
|
+
const sensors = useSensors(
|
|
62
|
+
useSensor(PointerSensor, {
|
|
63
|
+
activationConstraint: { distance: 8 },
|
|
64
|
+
})
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Get active sprint
|
|
68
|
+
const activeSprint = useMemo(
|
|
69
|
+
() => sprints.find((s) => s.status === SprintStatus.ACTIVE),
|
|
70
|
+
[sprints]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Filter tasks
|
|
74
|
+
const filteredTasks = useMemo(() => {
|
|
75
|
+
return tasks.filter((task) => {
|
|
76
|
+
// Sprint filter (Always show active sprint only if it exists)
|
|
77
|
+
if (activeSprint) {
|
|
78
|
+
if (task.sprintId !== activeSprint.id) return false;
|
|
79
|
+
} else {
|
|
80
|
+
// If no active sprint, show nothing on board (it will hit empty state)
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
// Search filter
|
|
84
|
+
if (
|
|
85
|
+
searchQuery &&
|
|
86
|
+
!task.title.toLowerCase().includes(searchQuery.toLowerCase())
|
|
87
|
+
) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
// Priority filter
|
|
91
|
+
if (priorityFilter && task.priority !== priorityFilter) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// Role filter
|
|
95
|
+
if (roleFilter && task.assigneeRole !== roleFilter) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}, [tasks, activeSprint, searchQuery, priorityFilter, roleFilter]);
|
|
101
|
+
|
|
102
|
+
// Group by status
|
|
103
|
+
const tasksByStatus = useMemo(() => {
|
|
104
|
+
return BOARD_STATUSES.reduce(
|
|
105
|
+
(acc, status) => {
|
|
106
|
+
acc[status.key] = filteredTasks.filter((t) => t.status === status.key);
|
|
107
|
+
return acc;
|
|
108
|
+
},
|
|
109
|
+
{} as Record<TaskStatus, Task[]>
|
|
110
|
+
);
|
|
111
|
+
}, [filteredTasks]);
|
|
112
|
+
|
|
113
|
+
const handleDragStart = (event: DragStartEvent) => {
|
|
114
|
+
const task = tasks.find((t) => t.id === event.active.id);
|
|
115
|
+
setActiveTask(task || null);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const handleDragEnd = async (event: DragEndEvent) => {
|
|
119
|
+
const { active, over } = event;
|
|
120
|
+
setActiveTask(null);
|
|
121
|
+
|
|
122
|
+
if (!over) return;
|
|
123
|
+
|
|
124
|
+
const taskId = active.id as string;
|
|
125
|
+
const overId = over.id as string;
|
|
126
|
+
|
|
127
|
+
// Resolve target status
|
|
128
|
+
const overTask = tasks.find((t) => t.id === overId);
|
|
129
|
+
const newStatus = overTask ? overTask.status : (overId as TaskStatus);
|
|
130
|
+
|
|
131
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
132
|
+
if (!task || task.status === newStatus) return;
|
|
133
|
+
|
|
134
|
+
// Optimistic update
|
|
135
|
+
const previousTasks = queryClient.getQueryData<Task[]>(
|
|
136
|
+
queryKeys.tasks.list(workspaceId)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (previousTasks) {
|
|
140
|
+
queryClient.setQueryData<Task[]>(
|
|
141
|
+
queryKeys.tasks.list(workspaceId),
|
|
142
|
+
previousTasks.map((t) =>
|
|
143
|
+
t.id === taskId ? { ...t, status: newStatus } : t
|
|
144
|
+
)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
await locusClient.tasks.update(taskId, workspaceId, {
|
|
150
|
+
status: newStatus,
|
|
151
|
+
});
|
|
152
|
+
// No need to refetch if optimistic update is successful,
|
|
153
|
+
// but we should eventually to stay in sync.
|
|
154
|
+
// queryClient.invalidateQueries({ queryKey: queryKeys.tasks.list(workspaceId) });
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// Rollback
|
|
157
|
+
if (previousTasks) {
|
|
158
|
+
queryClient.setQueryData(
|
|
159
|
+
queryKeys.tasks.list(workspaceId),
|
|
160
|
+
previousTasks
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
toast.error(
|
|
164
|
+
error instanceof Error ? error.message : "Failed to update task"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handleDeleteTask = async (taskId: string) => {
|
|
170
|
+
const previousTasks = queryClient.getQueryData<Task[]>(
|
|
171
|
+
queryKeys.tasks.list(workspaceId)
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (previousTasks) {
|
|
175
|
+
queryClient.setQueryData<Task[]>(
|
|
176
|
+
queryKeys.tasks.list(workspaceId),
|
|
177
|
+
previousTasks.filter((t) => t.id !== taskId)
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await locusClient.tasks.delete(taskId, workspaceId);
|
|
183
|
+
toast.success("Task deleted");
|
|
184
|
+
queryClient.invalidateQueries({
|
|
185
|
+
queryKey: queryKeys.tasks.list(workspaceId),
|
|
186
|
+
});
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// Rollback
|
|
189
|
+
if (previousTasks) {
|
|
190
|
+
queryClient.setQueryData(
|
|
191
|
+
queryKeys.tasks.list(workspaceId),
|
|
192
|
+
previousTasks
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
toast.error(
|
|
196
|
+
error instanceof Error ? error.message : "Failed to delete task"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const isLoading = tasksLoading || sprintsLoading;
|
|
202
|
+
const shouldShowEmptyState = !activeSprint;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
tasks,
|
|
206
|
+
activeSprint,
|
|
207
|
+
filteredTasks,
|
|
208
|
+
tasksByStatus,
|
|
209
|
+
activeTask,
|
|
210
|
+
isLoading,
|
|
211
|
+
shouldShowEmptyState,
|
|
212
|
+
isCreateModalOpen,
|
|
213
|
+
setIsCreateModalOpen,
|
|
214
|
+
selectedTaskId,
|
|
215
|
+
setSelectedTaskId,
|
|
216
|
+
searchQuery,
|
|
217
|
+
setSearchQuery,
|
|
218
|
+
priorityFilter,
|
|
219
|
+
setPriorityFilter,
|
|
220
|
+
roleFilter,
|
|
221
|
+
setRoleFilter,
|
|
222
|
+
view,
|
|
223
|
+
setView,
|
|
224
|
+
sensors,
|
|
225
|
+
handleDragStart,
|
|
226
|
+
handleDragEnd,
|
|
227
|
+
handleDeleteTask,
|
|
228
|
+
refetch,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for dashboard layout routing and protection logic
|
|
3
|
+
*
|
|
4
|
+
* Handles redirecting unauthenticated users away from dashboard
|
|
5
|
+
* and managing the authenticated dashboard UI state
|
|
6
|
+
*/
|
|
7
|
+
"use client";
|
|
8
|
+
|
|
9
|
+
import { useRouter } from "next/navigation";
|
|
10
|
+
import { useEffect } from "react";
|
|
11
|
+
import { useAuth } from "@/context/AuthContext";
|
|
12
|
+
import { isCloudMode } from "@/utils/env.utils";
|
|
13
|
+
|
|
14
|
+
interface UseDashboardLayoutReturn {
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
isAuthenticated: boolean;
|
|
17
|
+
shouldShowUI: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Manages dashboard layout routing and protection
|
|
22
|
+
* Returns flags to determine what to render
|
|
23
|
+
*
|
|
24
|
+
* @returns Dashboard layout state flags
|
|
25
|
+
*/
|
|
26
|
+
export function useDashboardLayout(): UseDashboardLayoutReturn {
|
|
27
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
// Skip redirect if still loading or not in cloud mode
|
|
32
|
+
if (isLoading || !isCloudMode()) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Redirect unauthenticated users to login
|
|
37
|
+
if (!isAuthenticated) {
|
|
38
|
+
router.push("/login");
|
|
39
|
+
}
|
|
40
|
+
}, [isLoading, isAuthenticated, router]);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
isLoading,
|
|
44
|
+
isAuthenticated,
|
|
45
|
+
// Show UI only if authenticated or still loading
|
|
46
|
+
// Hide UI if unauthenticated (to avoid flash before redirect)
|
|
47
|
+
shouldShowUI: isAuthenticated || isLoading,
|
|
48
|
+
};
|
|
49
|
+
}
|