kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cd7cada2f86b4e866a15b4323bb8d6d7ab5bba8b 193 lines 4.9 kB view raw
1import { and, asc, eq, max } from "drizzle-orm"; 2import { HTTPException } from "hono/http-exception"; 3import db from "../../database"; 4import { 5 assetTable, 6 columnTable, 7 projectTable, 8 taskTable, 9} from "../../database/schema"; 10import { publishEvent } from "../../events"; 11import getNextTaskNumber from "./get-next-task-number"; 12 13type DbOrTx = typeof db | Parameters<Parameters<typeof db.transaction>[0]>[0]; 14 15function isSameProjectMove( 16 sourceProjectId: string, 17 destinationProjectId: string, 18) { 19 return sourceProjectId === destinationProjectId; 20} 21 22async function resolveDestinationStatus( 23 destinationProjectId: string, 24 currentStatus: string, 25 requestedStatus?: string, 26) { 27 const destinationColumns = await db 28 .select({ 29 id: columnTable.id, 30 slug: columnTable.slug, 31 position: columnTable.position, 32 }) 33 .from(columnTable) 34 .where(eq(columnTable.projectId, destinationProjectId)) 35 .orderBy(asc(columnTable.position)); 36 37 if (destinationColumns.length === 0) { 38 throw new HTTPException(400, { 39 message: "Destination project does not have a workflow", 40 }); 41 } 42 43 const requestedColumn = requestedStatus 44 ? destinationColumns.find((column) => column.slug === requestedStatus) 45 : null; 46 47 if (requestedStatus && !requestedColumn) { 48 throw new HTTPException(400, { 49 message: "Selected status is not valid for the destination project", 50 }); 51 } 52 53 const matchingCurrentColumn = destinationColumns.find( 54 (column) => column.slug === currentStatus, 55 ); 56 57 return requestedColumn ?? matchingCurrentColumn ?? destinationColumns[0]; 58} 59 60async function getNextTaskPosition( 61 dbOrTx: DbOrTx, 62 projectId: string, 63 status: string, 64 columnId: string, 65) { 66 const [maxPositionResult] = await dbOrTx 67 .select({ maxPosition: max(taskTable.position) }) 68 .from(taskTable) 69 .where( 70 and( 71 eq(taskTable.projectId, projectId), 72 eq(taskTable.status, status), 73 eq(taskTable.columnId, columnId), 74 ), 75 ); 76 77 return (maxPositionResult?.maxPosition ?? 0) + 1; 78} 79 80async function moveTask({ 81 taskId, 82 destinationProjectId, 83 destinationStatus, 84 userId, 85}: { 86 taskId: string; 87 destinationProjectId: string; 88 destinationStatus?: string; 89 userId: string; 90}) { 91 const existingTask = await db.query.taskTable.findFirst({ 92 where: eq(taskTable.id, taskId), 93 }); 94 95 if (!existingTask) { 96 throw new HTTPException(404, { 97 message: "Task not found", 98 }); 99 } 100 101 if (isSameProjectMove(existingTask.projectId, destinationProjectId)) { 102 throw new HTTPException(400, { 103 message: "Task is already in that project", 104 }); 105 } 106 107 const [sourceProject, destinationProject] = await Promise.all([ 108 db.query.projectTable.findFirst({ 109 where: eq(projectTable.id, existingTask.projectId), 110 }), 111 db.query.projectTable.findFirst({ 112 where: eq(projectTable.id, destinationProjectId), 113 }), 114 ]); 115 116 if (!sourceProject || !destinationProject) { 117 throw new HTTPException(404, { 118 message: "Project not found", 119 }); 120 } 121 122 if (sourceProject.workspaceId !== destinationProject.workspaceId) { 123 throw new HTTPException(400, { 124 message: "Tasks can only be moved within the same workspace", 125 }); 126 } 127 128 const resolvedColumn = await resolveDestinationStatus( 129 destinationProjectId, 130 existingTask.status, 131 destinationStatus, 132 ); 133 134 const movedTask = await db.transaction(async (tx) => { 135 const [nextTaskNumber, nextPosition] = await Promise.all([ 136 getNextTaskNumber(destinationProjectId, tx), 137 getNextTaskPosition( 138 tx, 139 destinationProjectId, 140 resolvedColumn.slug, 141 resolvedColumn.id, 142 ), 143 ]); 144 145 const [updatedTask] = await tx 146 .update(taskTable) 147 .set({ 148 projectId: destinationProjectId, 149 status: resolvedColumn.slug, 150 columnId: resolvedColumn.id, 151 number: nextTaskNumber + 1, 152 position: nextPosition, 153 }) 154 .where(eq(taskTable.id, taskId)) 155 .returning(); 156 157 if (!updatedTask) { 158 throw new HTTPException(500, { 159 message: "Failed to move task", 160 }); 161 } 162 163 await tx 164 .update(assetTable) 165 .set({ projectId: destinationProjectId }) 166 .where(eq(assetTable.taskId, taskId)); 167 168 return updatedTask; 169 }); 170 171 await publishEvent("task.moved", { 172 taskId, 173 type: "task", 174 userId, 175 content: `Moved task from ${sourceProject.name} to ${destinationProject.name}`, 176 eventData: { 177 fromProjectId: sourceProject.id, 178 fromProjectName: sourceProject.name, 179 toProjectId: destinationProject.id, 180 toProjectName: destinationProject.name, 181 oldStatus: existingTask.status, 182 newStatus: resolvedColumn.slug, 183 }, 184 }); 185 186 return { 187 task: movedTask, 188 sourceProjectId: sourceProject.id, 189 destinationProjectId: destinationProject.id, 190 }; 191} 192 193export default moveTask;