@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,341 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type AcceptanceItem, type Task, TaskStatus } from "@locusai/shared";
|
|
4
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { toast } from "sonner";
|
|
7
|
+
import { useAuth } from "@/context/AuthContext";
|
|
8
|
+
import { useWorkspaceId } from "@/hooks/useWorkspaceId";
|
|
9
|
+
import { locusClient } from "@/lib/api-client";
|
|
10
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
11
|
+
|
|
12
|
+
interface UseTaskPanelProps {
|
|
13
|
+
taskId: string;
|
|
14
|
+
onUpdated: () => void;
|
|
15
|
+
onDeleted: () => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Task Panel Hook
|
|
21
|
+
* Handles detailed task view, checklist logic, and operations.
|
|
22
|
+
*
|
|
23
|
+
* Features:
|
|
24
|
+
* - Optimistic updates for instant UI feedback
|
|
25
|
+
* - Support for assignedTo and dueDate fields
|
|
26
|
+
* - Loading states for all mutations
|
|
27
|
+
*/
|
|
28
|
+
export function useTaskPanel({
|
|
29
|
+
taskId,
|
|
30
|
+
onUpdated,
|
|
31
|
+
onDeleted,
|
|
32
|
+
onClose,
|
|
33
|
+
}: UseTaskPanelProps) {
|
|
34
|
+
const { user } = useAuth();
|
|
35
|
+
const workspaceId = useWorkspaceId();
|
|
36
|
+
const queryClient = useQueryClient();
|
|
37
|
+
|
|
38
|
+
// Fetch task details
|
|
39
|
+
const { data: task, refetch: fetchTask } = useQuery({
|
|
40
|
+
queryKey: queryKeys.tasks.detail(taskId),
|
|
41
|
+
queryFn: () => locusClient.tasks.getById(taskId, workspaceId),
|
|
42
|
+
enabled: !!workspaceId,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// UI State
|
|
46
|
+
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
|
47
|
+
const [editTitle, setEditTitle] = useState("");
|
|
48
|
+
const [editDesc, setEditDesc] = useState("");
|
|
49
|
+
const [editAssignedTo, setEditAssignedTo] = useState("");
|
|
50
|
+
const [editDueDate, setEditDueDate] = useState("");
|
|
51
|
+
const [newComment, setNewComment] = useState("");
|
|
52
|
+
const [newChecklistItem, setNewChecklistItem] = useState("");
|
|
53
|
+
const [descMode, setDescMode] = useState<"edit" | "preview">("preview");
|
|
54
|
+
const [showRejectModal, setShowRejectModal] = useState(false);
|
|
55
|
+
const [rejectReason, setRejectReason] = useState("");
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (task) {
|
|
59
|
+
setEditTitle(task.title);
|
|
60
|
+
setEditDesc(task.description || "");
|
|
61
|
+
setEditAssignedTo(task.assignedTo || "");
|
|
62
|
+
setEditDueDate(
|
|
63
|
+
task.dueDate ? new Date(task.dueDate).toISOString().split("T")[0] : ""
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}, [task]);
|
|
67
|
+
|
|
68
|
+
// Mutations with Optimistic Updates
|
|
69
|
+
const updateTaskMutation = useMutation({
|
|
70
|
+
mutationFn: (updates: Partial<Task> & { docIds?: string[] }) =>
|
|
71
|
+
locusClient.tasks.update(taskId, workspaceId as string, updates),
|
|
72
|
+
// Optimistic update: update cache immediately
|
|
73
|
+
onMutate: async (updates) => {
|
|
74
|
+
// Cancel ongoing queries
|
|
75
|
+
await queryClient.cancelQueries({
|
|
76
|
+
queryKey: queryKeys.tasks.detail(taskId),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Snapshot previous data
|
|
80
|
+
const previousTask = queryClient.getQueryData<Task>(
|
|
81
|
+
queryKeys.tasks.detail(taskId)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Update cache optimistically
|
|
85
|
+
if (previousTask) {
|
|
86
|
+
queryClient.setQueryData(queryKeys.tasks.detail(taskId), {
|
|
87
|
+
...previousTask,
|
|
88
|
+
...updates,
|
|
89
|
+
dueDate: updates.dueDate
|
|
90
|
+
? new Date(updates.dueDate)
|
|
91
|
+
: previousTask.dueDate,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { previousTask };
|
|
96
|
+
},
|
|
97
|
+
// On error, rollback to previous data
|
|
98
|
+
onError: (_err, _variables, context) => {
|
|
99
|
+
if (context?.previousTask) {
|
|
100
|
+
queryClient.setQueryData(
|
|
101
|
+
queryKeys.tasks.detail(taskId),
|
|
102
|
+
context.previousTask
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
toast.error("Failed to update task");
|
|
106
|
+
},
|
|
107
|
+
// On success, refetch to confirm
|
|
108
|
+
onSuccess: () => {
|
|
109
|
+
fetchTask();
|
|
110
|
+
onUpdated();
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const handleLinkDoc = async (docId: string) => {
|
|
115
|
+
if (!task) return;
|
|
116
|
+
const currentDocIds = task.docs?.map((d) => d.id) || [];
|
|
117
|
+
if (currentDocIds.includes(docId)) return;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await updateTaskMutation.mutateAsync({
|
|
121
|
+
docIds: [...currentDocIds, docId],
|
|
122
|
+
});
|
|
123
|
+
toast.success("Document linked");
|
|
124
|
+
} catch {
|
|
125
|
+
toast.error("Failed to link document");
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const handleUnlinkDoc = async (docId: string) => {
|
|
130
|
+
if (!task) return;
|
|
131
|
+
const currentDocIds = task.docs?.map((d) => d.id) || [];
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await updateTaskMutation.mutateAsync({
|
|
135
|
+
docIds: currentDocIds.filter((id) => id !== docId),
|
|
136
|
+
});
|
|
137
|
+
toast.success("Document unlinked");
|
|
138
|
+
} catch {
|
|
139
|
+
toast.error("Failed to unlink document");
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const deleteTaskMutation = useMutation({
|
|
144
|
+
mutationFn: () => locusClient.tasks.delete(taskId, workspaceId as string),
|
|
145
|
+
onSuccess: () => {
|
|
146
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.tasks.all() });
|
|
147
|
+
toast.success("Task deleted");
|
|
148
|
+
onDeleted();
|
|
149
|
+
onClose();
|
|
150
|
+
},
|
|
151
|
+
onError: () => {
|
|
152
|
+
toast.error("Failed to delete task");
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const addCommentMutation = useMutation({
|
|
157
|
+
mutationFn: (data: { author: string; text: string }) =>
|
|
158
|
+
locusClient.tasks.addComment(taskId, workspaceId as string, data),
|
|
159
|
+
onSuccess: () => {
|
|
160
|
+
fetchTask();
|
|
161
|
+
},
|
|
162
|
+
onError: () => {
|
|
163
|
+
toast.error("Failed to add comment");
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const handleUpdateTask = async (updates: Partial<Task>) => {
|
|
168
|
+
try {
|
|
169
|
+
await updateTaskMutation.mutateAsync(updates);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error("Failed to update task:", err);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleDelete = async () => {
|
|
176
|
+
if (
|
|
177
|
+
!confirm(
|
|
178
|
+
"Are you sure you want to delete this task? This action cannot be undone."
|
|
179
|
+
)
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await deleteTaskMutation.mutateAsync();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error("Failed to delete task:", err);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const handleTitleSave = () => {
|
|
191
|
+
if (editTitle.trim() && editTitle !== task?.title) {
|
|
192
|
+
handleUpdateTask({ title: editTitle.trim() });
|
|
193
|
+
}
|
|
194
|
+
setIsEditingTitle(false);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleDescSave = () => {
|
|
198
|
+
if (editDesc !== task?.description) {
|
|
199
|
+
handleUpdateTask({ description: editDesc });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const handleAssignedToSave = () => {
|
|
204
|
+
if (editAssignedTo !== (task?.assignedTo || "")) {
|
|
205
|
+
handleUpdateTask({ assignedTo: editAssignedTo || null });
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const handleDueDateSave = () => {
|
|
210
|
+
if (editDueDate) {
|
|
211
|
+
const newDate = new Date(editDueDate);
|
|
212
|
+
handleUpdateTask({ dueDate: newDate });
|
|
213
|
+
} else if (task?.dueDate) {
|
|
214
|
+
handleUpdateTask({ dueDate: null });
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleAddChecklistItem = () => {
|
|
219
|
+
if (!newChecklistItem.trim() || !task) return;
|
|
220
|
+
const newItem: AcceptanceItem = {
|
|
221
|
+
id: crypto.randomUUID(),
|
|
222
|
+
text: newChecklistItem.trim(),
|
|
223
|
+
done: false,
|
|
224
|
+
};
|
|
225
|
+
handleUpdateTask({
|
|
226
|
+
acceptanceChecklist: [...(task.acceptanceChecklist || []), newItem],
|
|
227
|
+
});
|
|
228
|
+
setNewChecklistItem("");
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleToggleChecklistItem = (itemId: string) => {
|
|
232
|
+
if (!task?.acceptanceChecklist) return;
|
|
233
|
+
const updated = task.acceptanceChecklist.map((item) =>
|
|
234
|
+
item.id === itemId ? { ...item, done: !item.done } : item
|
|
235
|
+
);
|
|
236
|
+
handleUpdateTask({ acceptanceChecklist: updated });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const handleRemoveChecklistItem = (itemId: string) => {
|
|
240
|
+
if (!task?.acceptanceChecklist) return;
|
|
241
|
+
const updated = task.acceptanceChecklist.filter(
|
|
242
|
+
(item) => item.id !== itemId
|
|
243
|
+
);
|
|
244
|
+
handleUpdateTask({ acceptanceChecklist: updated });
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const handleAddComment = async () => {
|
|
248
|
+
if (!newComment.trim()) return;
|
|
249
|
+
try {
|
|
250
|
+
await addCommentMutation.mutateAsync({
|
|
251
|
+
author: user?.name || "Anonymous",
|
|
252
|
+
text: newComment,
|
|
253
|
+
});
|
|
254
|
+
setNewComment("");
|
|
255
|
+
toast.success("Comment added");
|
|
256
|
+
} catch (err) {
|
|
257
|
+
console.error("Failed to add comment:", err);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const handleReject = async () => {
|
|
262
|
+
if (!rejectReason.trim()) return;
|
|
263
|
+
try {
|
|
264
|
+
await updateTaskMutation.mutateAsync({
|
|
265
|
+
status: TaskStatus.IN_PROGRESS,
|
|
266
|
+
});
|
|
267
|
+
await addCommentMutation.mutateAsync({
|
|
268
|
+
author: user?.name || "Manager",
|
|
269
|
+
text: `❌ **Rejected**: ${rejectReason}`,
|
|
270
|
+
});
|
|
271
|
+
setShowRejectModal(false);
|
|
272
|
+
setRejectReason("");
|
|
273
|
+
toast.success("Task rejected and moved back to in progress");
|
|
274
|
+
onUpdated();
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error("Failed to reject task:", err);
|
|
277
|
+
toast.error("Failed to reject task");
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const handleApprove = async () => {
|
|
282
|
+
try {
|
|
283
|
+
await updateTaskMutation.mutateAsync({ status: TaskStatus.DONE });
|
|
284
|
+
toast.success("Task approved and marked as done");
|
|
285
|
+
onUpdated();
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.error("Failed to approve task:", err);
|
|
288
|
+
toast.error("Failed to approve task");
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const checklistProgress = task?.acceptanceChecklist?.length
|
|
293
|
+
? Math.round(
|
|
294
|
+
(task.acceptanceChecklist.filter((i) => i.done).length /
|
|
295
|
+
task.acceptanceChecklist.length) *
|
|
296
|
+
100
|
|
297
|
+
)
|
|
298
|
+
: 0;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
task,
|
|
302
|
+
isEditingTitle,
|
|
303
|
+
setIsEditingTitle,
|
|
304
|
+
editTitle,
|
|
305
|
+
setEditTitle,
|
|
306
|
+
editDesc,
|
|
307
|
+
setEditDesc,
|
|
308
|
+
editAssignedTo,
|
|
309
|
+
setEditAssignedTo,
|
|
310
|
+
editDueDate,
|
|
311
|
+
setEditDueDate,
|
|
312
|
+
newComment,
|
|
313
|
+
setNewComment,
|
|
314
|
+
newChecklistItem,
|
|
315
|
+
setNewChecklistItem,
|
|
316
|
+
descMode,
|
|
317
|
+
setDescMode,
|
|
318
|
+
showRejectModal,
|
|
319
|
+
setShowRejectModal,
|
|
320
|
+
rejectReason,
|
|
321
|
+
setRejectReason,
|
|
322
|
+
checklistProgress,
|
|
323
|
+
isLoading: updateTaskMutation.isPending || addCommentMutation.isPending,
|
|
324
|
+
isDeleting: deleteTaskMutation.isPending,
|
|
325
|
+
handleUpdateTask,
|
|
326
|
+
handleLinkDoc,
|
|
327
|
+
handleUnlinkDoc,
|
|
328
|
+
handleDelete,
|
|
329
|
+
handleTitleSave,
|
|
330
|
+
handleDescSave,
|
|
331
|
+
handleAssignedToSave,
|
|
332
|
+
handleDueDateSave,
|
|
333
|
+
handleAddChecklistItem,
|
|
334
|
+
handleToggleChecklistItem,
|
|
335
|
+
handleRemoveChecklistItem,
|
|
336
|
+
handleAddComment,
|
|
337
|
+
handleReject,
|
|
338
|
+
handleApprove,
|
|
339
|
+
refresh: fetchTask,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type Task } from "@locusai/shared";
|
|
4
|
+
import { useQuery } from "@tanstack/react-query";
|
|
5
|
+
import { locusClient } from "@/lib/api-client";
|
|
6
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
7
|
+
import { useWorkspaceIdOptional } from "./useWorkspaceId";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tasks Query Hook
|
|
11
|
+
* Centralizes task fetching logic.
|
|
12
|
+
*/
|
|
13
|
+
export function useTasksQuery() {
|
|
14
|
+
const workspaceId = useWorkspaceIdOptional();
|
|
15
|
+
|
|
16
|
+
return useQuery<Task[]>({
|
|
17
|
+
queryKey: queryKeys.tasks.list(workspaceId),
|
|
18
|
+
queryFn: () =>
|
|
19
|
+
workspaceId ? locusClient.tasks.list(workspaceId) : Promise.resolve([]),
|
|
20
|
+
enabled: !!workspaceId,
|
|
21
|
+
refetchInterval: 10_000,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Backlog Query Hook
|
|
27
|
+
*/
|
|
28
|
+
export function useBacklogQuery() {
|
|
29
|
+
const workspaceId = useWorkspaceIdOptional();
|
|
30
|
+
|
|
31
|
+
return useQuery<Task[]>({
|
|
32
|
+
queryKey: queryKeys.tasks.backlog(workspaceId),
|
|
33
|
+
queryFn: () =>
|
|
34
|
+
workspaceId
|
|
35
|
+
? locusClient.tasks.getBacklog(workspaceId)
|
|
36
|
+
: Promise.resolve([]),
|
|
37
|
+
enabled: !!workspaceId,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for team management logic
|
|
3
|
+
* Handles members, invitations, and member operations
|
|
4
|
+
*/
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { MembershipRole } from "@locusai/shared";
|
|
8
|
+
import { useQueryClient } from "@tanstack/react-query";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import {
|
|
12
|
+
useAuthenticatedUserWithOrg,
|
|
13
|
+
useInvitationsQuery,
|
|
14
|
+
useOrganizationMembersQuery,
|
|
15
|
+
} from "@/hooks";
|
|
16
|
+
import { locusClient } from "@/lib/api-client";
|
|
17
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
18
|
+
|
|
19
|
+
export function useTeamManagement() {
|
|
20
|
+
const currentUser = useAuthenticatedUserWithOrg();
|
|
21
|
+
const { data: members = [], isLoading: membersLoading } =
|
|
22
|
+
useOrganizationMembersQuery();
|
|
23
|
+
const queryClient = useQueryClient();
|
|
24
|
+
const [isInviteModalOpen, setIsInviteModalOpen] = useState(false);
|
|
25
|
+
|
|
26
|
+
// Determine user's role
|
|
27
|
+
const userMembership = members.find((m) => m.userId === currentUser?.id);
|
|
28
|
+
const isOwner = userMembership?.role === MembershipRole.OWNER;
|
|
29
|
+
const isAdmin = userMembership?.role === MembershipRole.ADMIN;
|
|
30
|
+
const canManage = isOwner || isAdmin;
|
|
31
|
+
|
|
32
|
+
const { data: invitations = [], isLoading: invitationsLoading } =
|
|
33
|
+
useInvitationsQuery({ enabled: canManage });
|
|
34
|
+
|
|
35
|
+
const handleRemoveMember = async (userId: string) => {
|
|
36
|
+
if (!confirm("Are you sure you want to remove this member?")) return;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
await locusClient.organizations.removeMember(currentUser.orgId, userId);
|
|
40
|
+
toast.success("Member removed");
|
|
41
|
+
queryClient.invalidateQueries({
|
|
42
|
+
queryKey: queryKeys.organizations.members(currentUser.orgId),
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
toast.error(
|
|
46
|
+
error instanceof Error ? error.message : "Failed to remove member"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleRevokeInvitation = async (invitationId: string) => {
|
|
52
|
+
try {
|
|
53
|
+
await locusClient.invitations.revoke(currentUser.orgId, invitationId);
|
|
54
|
+
toast.success("Invitation revoked");
|
|
55
|
+
queryClient.invalidateQueries({
|
|
56
|
+
queryKey: queryKeys.invitations.list(currentUser.orgId),
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
toast.error(
|
|
60
|
+
error instanceof Error ? error.message : "Failed to revoke invitation"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleCopyLink = (token: string) => {
|
|
66
|
+
const baseUrl =
|
|
67
|
+
typeof window !== "undefined"
|
|
68
|
+
? window.location.origin
|
|
69
|
+
: "https://app.locusai.dev";
|
|
70
|
+
const inviteUrl = `${baseUrl}/invite?token=${token}`;
|
|
71
|
+
|
|
72
|
+
navigator.clipboard.writeText(inviteUrl);
|
|
73
|
+
toast.success("Invitation link copied to clipboard");
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const isLoading = membersLoading || invitationsLoading;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
currentUser,
|
|
80
|
+
members,
|
|
81
|
+
invitations,
|
|
82
|
+
isLoading,
|
|
83
|
+
isOwner,
|
|
84
|
+
isAdmin,
|
|
85
|
+
canManage,
|
|
86
|
+
isInviteModalOpen,
|
|
87
|
+
setIsInviteModalOpen,
|
|
88
|
+
handleRemoveMember,
|
|
89
|
+
handleRevokeInvitation,
|
|
90
|
+
handleCopyLink,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for workspace creation form management
|
|
3
|
+
* Handles workspace creation with optional auto-organization creation
|
|
4
|
+
*/
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
8
|
+
import { useRouter } from "next/navigation";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { toast } from "sonner";
|
|
11
|
+
import { useAuth } from "@/context/AuthContext";
|
|
12
|
+
import { locusClient } from "@/lib/api-client";
|
|
13
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
14
|
+
import { useAuthenticatedUser } from "./useAuthenticatedUser";
|
|
15
|
+
|
|
16
|
+
interface UseWorkspaceCreateFormReturn {
|
|
17
|
+
name: string;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
setName: (name: string) => void;
|
|
20
|
+
handleSubmit: (e: React.FormEvent) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Custom hook for workspace creation
|
|
25
|
+
* Handles both standard and auto-org workspace creation
|
|
26
|
+
*/
|
|
27
|
+
export function useWorkspaceCreateForm(): UseWorkspaceCreateFormReturn {
|
|
28
|
+
const [name, setName] = useState("");
|
|
29
|
+
const router = useRouter();
|
|
30
|
+
const queryClient = useQueryClient();
|
|
31
|
+
const user = useAuthenticatedUser();
|
|
32
|
+
const { refreshUser } = useAuth();
|
|
33
|
+
|
|
34
|
+
const createWorkspaceMutation = useMutation({
|
|
35
|
+
mutationFn: (workspaceName: string) => {
|
|
36
|
+
// If user has orgId, use the standard create method
|
|
37
|
+
if (user?.orgId) {
|
|
38
|
+
return locusClient.workspaces.create({
|
|
39
|
+
name: workspaceName,
|
|
40
|
+
orgId: user.orgId,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Otherwise, use createWithAutoOrg which creates organization if needed
|
|
44
|
+
return locusClient.workspaces.createWithAutoOrg({ name: workspaceName });
|
|
45
|
+
},
|
|
46
|
+
onSuccess: async () => {
|
|
47
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.workspaces.all() });
|
|
48
|
+
// Refresh user data to get the new workspaceId
|
|
49
|
+
await refreshUser();
|
|
50
|
+
toast.success("Workspace created!");
|
|
51
|
+
router.push("/");
|
|
52
|
+
},
|
|
53
|
+
onError: (error: Error) => {
|
|
54
|
+
toast.error(error.message || "Failed to create workspace");
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
59
|
+
e.preventDefault();
|
|
60
|
+
if (!name.trim()) return;
|
|
61
|
+
createWorkspaceMutation.mutate(name.trim());
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
isLoading: createWorkspaceMutation.isPending,
|
|
67
|
+
setName,
|
|
68
|
+
handleSubmit,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAuth } from "@/context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get the current workspace ID
|
|
7
|
+
* Ensures consistent handling of workspace context across the app
|
|
8
|
+
*/
|
|
9
|
+
export function useWorkspaceId(): string {
|
|
10
|
+
const { user } = useAuth();
|
|
11
|
+
const workspaceId = user?.workspaceId;
|
|
12
|
+
|
|
13
|
+
if (!workspaceId) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"No workspace ID available. User must be authenticated with a workspace."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return workspaceId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook to safely get workspace ID or null
|
|
24
|
+
* Useful for conditional queries that shouldn't execute without a workspace
|
|
25
|
+
*/
|
|
26
|
+
export function useWorkspaceIdOptional(): string | null {
|
|
27
|
+
const { user } = useAuth();
|
|
28
|
+
return user?.workspaceId ?? null;
|
|
29
|
+
}
|
package/src/lib/api-client.ts
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { LocusClient, LocusEvent } from "@locusai/sdk";
|
|
2
|
+
import { config } from "./config";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
const API_BASE_URL =
|
|
5
|
-
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3080/api";
|
|
4
|
+
const tokenKey = "locus_token";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// Get initial token from localStorage if available
|
|
7
|
+
const initialToken =
|
|
8
|
+
typeof window !== "undefined" ? localStorage.getItem(tokenKey) : null;
|
|
9
|
+
|
|
10
|
+
export const locusClient = new LocusClient({
|
|
11
|
+
baseUrl: config.NEXT_PUBLIC_API_URL,
|
|
12
|
+
token: initialToken,
|
|
12
13
|
});
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
(
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
);
|
|
15
|
+
// Setup event listeners for the web app
|
|
16
|
+
if (typeof window !== "undefined") {
|
|
17
|
+
locusClient.emitter.on(LocusEvent.TOKEN_EXPIRED, () => {
|
|
18
|
+
localStorage.removeItem(tokenKey);
|
|
19
|
+
// Only redirect if not already on login/register/invite pages
|
|
20
|
+
if (!window.location.pathname.match(/\/(login|register|invite)/)) {
|
|
21
|
+
window.location.href = "/login";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
locusClient.emitter.on(LocusEvent.AUTH_ERROR, (error) => {
|
|
26
|
+
console.error("[Auth Error]", error);
|
|
27
|
+
});
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
locusClient.emitter.on(LocusEvent.REQUEST_ERROR, (error) => {
|
|
30
|
+
console.error("[Request Error]", error);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Helper to update token in client and localStorage
|
|
36
|
+
*/
|
|
37
|
+
export const setClientToken = (token: string | null) => {
|
|
38
|
+
if (token) {
|
|
39
|
+
localStorage.setItem(tokenKey, token);
|
|
40
|
+
locusClient.setToken(token);
|
|
41
|
+
} else {
|
|
42
|
+
localStorage.removeItem(tokenKey);
|
|
43
|
+
locusClient.setToken(null);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const ConfigSchema = z.object({
|
|
4
|
+
NEXT_PUBLIC_API_URL: z.string().url().default("http://localhost:3080/api"),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
8
|
+
|
|
9
|
+
function loadConfig(): Config {
|
|
10
|
+
// Safe parse for Next.js environment variables
|
|
11
|
+
const result = ConfigSchema.safeParse({
|
|
12
|
+
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!result.success) {
|
|
16
|
+
console.error("❌ Invalid environment variables:", result.error.format());
|
|
17
|
+
return {
|
|
18
|
+
NEXT_PUBLIC_API_URL: "http://localhost:3080/api",
|
|
19
|
+
} as Config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return result.data;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const config = loadConfig();
|