Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/app/oauth/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/features/project-docs/api/use-project-ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down Expand Up @@ -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");
}
Expand Down Expand Up @@ -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");
},
Expand Down
2 changes: 1 addition & 1 deletion src/features/project-teams/api/use-create-project-team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
79 changes: 42 additions & 37 deletions src/features/project-teams/server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectTeam>(
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<ProjectTeam>(
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);
}
}
)

Expand Down
15 changes: 15 additions & 0 deletions src/features/projects/server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Space>(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);
Expand Down
Loading