diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/projects/[projectId]/members/client.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/projects/[projectId]/members/client.tsx index abe5f14a..6af26c32 100644 --- a/src/app/(dashboard)/workspaces/[workspaceId]/projects/[projectId]/members/client.tsx +++ b/src/app/(dashboard)/workspaces/[workspaceId]/projects/[projectId]/members/client.tsx @@ -163,8 +163,8 @@ export const ProjectMembersClient = () => { const memberOptions = workspaceMembers // Exclude members already in the project .filter((member) => !existingProjectMemberUserIds.has(member.userId)) - // Exclude workspace OWNER (they have implicit access to all projects) - .filter((member) => member.role !== "OWNER") + // Exclude workspace OWNER and ADMIN (they have implicit access to all projects) + .filter((member) => member.role !== "OWNER" && member.role !== "ADMIN") .map((member) => ({ label: member.name || "Unknown", value: member.userId, diff --git a/src/app/oauth/route.ts b/src/app/oauth/route.ts index 2e76054e..d256f928 100644 --- a/src/app/oauth/route.ts +++ b/src/app/oauth/route.ts @@ -82,6 +82,7 @@ export async function GET(request: NextRequest) { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", + maxAge: 60 * 60 * 24 * 30, // 30 days }); // Redirect to unified callback for post-auth routing diff --git a/src/features/project-docs/api/use-project-ai.ts b/src/features/project-docs/api/use-project-ai.ts index 512798cb..3ee08f98 100644 --- a/src/features/project-docs/api/use-project-ai.ts +++ b/src/features/project-docs/api/use-project-ai.ts @@ -88,7 +88,7 @@ export const useAICreateTask = () => { }, onSuccess: (data) => { if (data.action?.executed && data.success) { - queryClient.invalidateQueries({ queryKey: ["tasks"] }); + queryClient.invalidateQueries({ queryKey: ["work-items"] }); queryClient.invalidateQueries({ queryKey: ["project-analytics"] }); toast.success(data.message || "Task created successfully"); } @@ -122,8 +122,8 @@ export const useAIUpdateTask = () => { }, onSuccess: (data) => { if (data.action?.executed && data.success) { - queryClient.invalidateQueries({ queryKey: ["tasks"] }); - queryClient.invalidateQueries({ queryKey: ["task", data.task?.id] }); + queryClient.invalidateQueries({ queryKey: ["work-items"] }); + queryClient.invalidateQueries({ queryKey: ["work-item", data.task?.id] }); queryClient.invalidateQueries({ queryKey: ["project-analytics"] }); toast.success(data.message || "Task updated successfully"); } @@ -171,10 +171,10 @@ export const useExecuteTaskSuggestion = () => { return await response.json() as AITaskResponse; }, onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["tasks"] }); + queryClient.invalidateQueries({ queryKey: ["work-items"] }); queryClient.invalidateQueries({ queryKey: ["project-analytics"] }); if (data.task?.id) { - queryClient.invalidateQueries({ queryKey: ["task", data.task.id] }); + queryClient.invalidateQueries({ queryKey: ["work-item", data.task.id] }); } toast.success(data.message || "Task operation completed"); }, diff --git a/src/features/project-teams/api/use-create-project-team.ts b/src/features/project-teams/api/use-create-project-team.ts index 8c65b0cf..5e9d9e18 100644 --- a/src/features/project-teams/api/use-create-project-team.ts +++ b/src/features/project-teams/api/use-create-project-team.ts @@ -15,7 +15,7 @@ export const useCreateProjectTeam = () => { const response = await client.api["project-teams"].$post({ json }); if (!response.ok) { - const error = await response.json(); + const error = await response.json().catch(() => ({ error: "Failed to create team" })); throw new Error((error as { error: string }).error || "Failed to create team"); } diff --git a/src/features/project-teams/server/route.ts b/src/features/project-teams/server/route.ts index 0884a86d..fbadc0c7 100644 --- a/src/features/project-teams/server/route.ts +++ b/src/features/project-teams/server/route.ts @@ -174,48 +174,53 @@ const app = new Hono() sessionMiddleware, zValidator("json", createProjectTeamSchema), async (c) => { - const { databases: adminDb } = await createAdminClient(); - const user = c.get("user"); - const data = c.req.valid("json"); - - // Check project access + permission - const access = await resolveUserProjectAccess(adminDb, user.$id, data.projectId); - if (!access.hasAccess) { - return c.json({ error: "Unauthorized: Not a project member" }, 403); - } - if (!hasProjectPermission(access, ProjectPermissionKey.MANAGE_TEAMS)) { - return c.json({ error: "Forbidden: Requires MANAGE_TEAMS permission" }, 403); - } + try { + const { databases: adminDb } = await createAdminClient(); + const user = c.get("user"); + const data = c.req.valid("json"); - // Check for duplicate team name - const existing = await adminDb.listDocuments( - DATABASE_ID, - PROJECT_TEAMS_ID, - [ - Query.equal("projectId", data.projectId), - Query.equal("name", data.name), - ] - ); + // Check project access + permission + const access = await resolveUserProjectAccess(adminDb, user.$id, data.projectId); + if (!access.hasAccess) { + return c.json({ error: "Unauthorized: Not a project member" }, 403); + } + if (!hasProjectPermission(access, ProjectPermissionKey.MANAGE_TEAMS)) { + return c.json({ error: "Forbidden: Requires MANAGE_TEAMS permission" }, 403); + } - if (existing.total > 0) { - return c.json({ error: "Team name already exists in this project" }, 409); - } + // Check for duplicate team name + const existing = await adminDb.listDocuments( + DATABASE_ID, + PROJECT_TEAMS_ID, + [ + Query.equal("projectId", data.projectId), + Query.equal("name", data.name), + ] + ); - // Create team - const team = await adminDb.createDocument( - DATABASE_ID, - PROJECT_TEAMS_ID, - ID.unique(), - { - projectId: data.projectId, - name: data.name, - description: data.description || null, - color: data.color || "#4F46E5", - createdBy: user.$id, + if (existing.total > 0) { + return c.json({ error: "Team name already exists in this project" }, 409); } - ); - return c.json({ data: team }, 201); + // Create team + const team = await adminDb.createDocument( + DATABASE_ID, + PROJECT_TEAMS_ID, + ID.unique(), + { + projectId: data.projectId, + name: data.name, + description: data.description || null, + color: data.color || "#4F46E5", + createdBy: user.$id, + } + ); + + return c.json({ data: team }, 201); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to create team"; + return c.json({ error: message }, 500); + } } ) diff --git a/src/features/projects/server/route.ts b/src/features/projects/server/route.ts index 7c81baea..684f38ef 100644 --- a/src/features/projects/server/route.ts +++ b/src/features/projects/server/route.ts @@ -324,6 +324,21 @@ const app = new Hono() updateData.workflowId = (workflowId === null || workflowId === "" || workflowId === "null") ? null : workflowId; } + // When removing a project from a space (spaceId → null) and workflowId was NOT + // explicitly provided, auto-clear the workflowId if it was inherited from the + // old space. This prevents stale workflow references from blocking kanban drags. + if (updateData.spaceId === null && workflowId === undefined && existingProject.spaceId && existingProject.workflowId) { + try { + const oldSpace = await databases.getDocument(DATABASE_ID, SPACES_ID, existingProject.spaceId); + if (oldSpace.defaultWorkflowId && existingProject.workflowId === oldSpace.defaultWorkflowId) { + updateData.workflowId = null; + } + } catch { + // Old space not found — safe to clear the orphaned workflowId + updateData.workflowId = null; + } + } + // Update custom definitions if (customWorkItemTypes !== undefined) { updateData.customWorkItemTypes = JSON.stringify(customWorkItemTypes);