Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
132 changes: 71 additions & 61 deletions apps/api/src/task/controllers/bulk-update-tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
projectTable,
taskTable,
userTable,
taskReminderSentTable,
workspaceUserTable,
} from "../../database/schema";
import { publishEvent } from "../../events";
import { removeLabelFromGitea } from "../../plugins/gitea/utils/sync-label-to-gitea";
import { removeLabelFromGitHub } from "../../plugins/github/utils/sync-label-to-github";
import { removeLabelFromGitlab } from "../../plugins/gitlab/utils/sync-label-to-gitlab";
import { assertAssignableUser } from "../../utils/assert-assignable-user";
import { publishTaskMutation } from "./task-mutation-effects";
import { getSubtaskParentProjects } from "../get-subtask-parent-projects";
import {
assertValidPriority,
Expand Down Expand Up @@ -169,17 +171,18 @@ async function bulkUpdateTasks({
});
for (const updatedTask of updatedTasks) {
const before = beforeById.get(updatedTask.id)!;
await publishEvent("task.status_changed", {
taskId: updatedTask.id,
projectId: updatedTask.projectId,
userId,
oldStatus: before.status,
newStatus: updatedTask.status,
title: updatedTask.title,
assigneeId: updatedTask.userId,
type: "status_changed",
await publishTaskMutation(before, updatedTask, userId, {
fields: ["status"],
skipRelationRefresh: true,

skipSubtaskParentRefresh: true,
});
if (before.status === updatedTask.status)
await publishEvent("task.updated", {
taskId: updatedTask.id,
projectId: updatedTask.projectId,
userId,
});
}
for (const projectId of projectIds)
await publishEvent("task-relation.refresh", { projectId, userId });
Expand All @@ -199,17 +202,17 @@ async function bulkUpdateTasks({

updatedCount = result.rowCount ?? foundIds.length;

for (const task of tasks) {
await publishEvent("task.priority_changed", {
taskId: task.id,
projectId: task.projectId,
for (const task of tasks)
await publishTaskMutation(
task,
{
...task,
description: null,
priority: value,
},
userId,
oldPriority: task.priority,
newPriority: value,
title: task.title,
type: "priority_changed",
});
}
{ fields: ["priority"] },
);
break;
}

Expand All @@ -220,38 +223,28 @@ async function bulkUpdateTasks({
await assertAssignableUser(assigneeId, workspaceId);
}

const newAssigneeName = assigneeId
? (
await db
.select({ name: userTable.name })
.from(userTable)
.where(eq(userTable.id, assigneeId))
.limit(1)
)[0]?.name
const assignee = assigneeId
? await db.query.userTable.findFirst({
columns: { name: true },
where: eq(userTable.id, assigneeId),
})
: undefined;

const result = await db
.update(taskTable)
.set({ userId: assigneeId })
.where(inArray(taskTable.id, foundIds));

updatedCount = result.rowCount ?? foundIds.length;

for (const task of tasks) {
const eventType = assigneeId
? "task.assignee_changed"
: "task.unassigned";
await publishEvent(eventType, {
taskId: task.id,
projectId: task.projectId,
for (const task of tasks)
await publishTaskMutation(
Comment thread
tinsever marked this conversation as resolved.
Outdated
task,
{
Comment thread
tinsever marked this conversation as resolved.
Outdated
...task,
description: null,
userId: assigneeId,
},
userId,
oldAssignee: task.userId,
newAssignee: newAssigneeName,
newAssigneeId: assigneeId,
title: task.title,
type: assigneeId ? "assignee_changed" : "unassigned",
});
}
{ fields: ["userId"], assigneeName: assignee?.name ?? null },
);
break;
}

Expand Down Expand Up @@ -401,24 +394,41 @@ async function bulkUpdateTasks({
}
}

const result = await db
.update(taskTable)
.set({ dueDate: parsedDate })
.where(inArray(taskTable.id, foundIds));

updatedCount = result.rowCount ?? foundIds.length;

for (const task of tasks) {
await publishEvent("task.due_date_changed", {
taskId: task.id,
projectId: task.projectId,
const { updatedTasks, beforeById } = await db.transaction(async (tx) => {
const before = await tx
.select({ id: taskTable.id, dueDate: taskTable.dueDate })
.from(taskTable)
.where(inArray(taskTable.id, foundIds))
.orderBy(asc(taskTable.id))
.for("update");
const beforeById = new Map(before.map((task) => [task.id, task]));
const changedIds = before
.filter((task) => task.dueDate?.getTime() !== parsedDate?.getTime())
.map((task) => task.id);
if (changedIds.length)
await tx
.delete(taskReminderSentTable)
.where(inArray(taskReminderSentTable.taskId, changedIds));
const updatedTasks = await tx
.update(taskTable)
.set({ dueDate: parsedDate })
.where(inArray(taskTable.id, foundIds))
.returning({
id: taskTable.id,
projectId: taskTable.projectId,
title: taskTable.title,
dueDate: taskTable.dueDate,
});
return { updatedTasks, beforeById };
});
updatedCount = updatedTasks.length;
for (const updatedTask of updatedTasks)
await publishTaskMutation(
beforeById.get(updatedTask.id)!,
updatedTask,
userId,
oldDueDate: task.dueDate,
newDueDate: parsedDate,
title: task.title,
type: "due_date_changed",
});
}
{ fields: ["dueDate"] },
);
break;
}

Expand Down
178 changes: 178 additions & 0 deletions apps/api/src/task/controllers/task-mutation-effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { eq } from "drizzle-orm";
import db from "../../database";
import {
activityTable,
taskReminderSentTable,
taskTable,
userTable,
} from "../../database/schema";
import { publishEvent } from "../../events";
import createNotification from "../../notification/controllers/create-notification";
import { deleteOrphanedAssets } from "../../storage/cleanup-assets";
import { parseMentionIds } from "../../utils/parse-mentions";

type Task = typeof taskTable.$inferSelect;
type Transaction = Parameters<Parameters<typeof db.transaction>[0]>[0];
type Changes = Partial<
Pick<
Task,
"title" | "status" | "priority" | "description" | "userId" | "dueDate"
>
>;
export type TaskBefore = Pick<
Task,
"id" | "projectId" | "title" | "status" | "priority" | "userId" | "dueDate"
> & { description?: string | null };

export async function recordTaskMutation(
tx: Transaction,
before: TaskBefore,
changes: Changes,
userId?: string,
) {
if (changes.title !== undefined && before.title !== changes.title) {
await tx.insert(activityTable).values({
taskId: before.id,
type: "title_changed",
userId: userId ?? null,
content: null,
eventData: { oldTitle: before.title, newTitle: changes.title },
});
}
if (
changes.dueDate !== undefined &&
before.dueDate?.getTime() !== changes.dueDate?.getTime()
) {
await tx
.delete(taskReminderSentTable)
.where(eq(taskReminderSentTable.taskId, before.id));
}
}

export async function publishTaskMutation(
before: Partial<TaskBefore>,
after: Pick<TaskBefore, "id" | "projectId" | "title"> & Partial<TaskBefore>,
userId?: string,
options: {
skipSubtaskParentRefresh?: boolean;
skipRelationRefresh?: boolean;
fields?: Array<keyof Changes>;
assigneeName?: string | null;
} = {},
) {
const changed = (field: keyof Changes) =>
(!options.fields || options.fields.includes(field)) &&
before[field] !== after[field];
Comment thread
tinsever marked this conversation as resolved.
const common = {
taskId: after.id,
projectId: after.projectId,
userId,
title: after.title,
};
if (changed("status")) {
await publishEvent("task.status_changed", {
...common,
oldStatus: before.status,
newStatus: after.status,
assigneeId: after.userId,
type: "status_changed",
...(options.skipSubtaskParentRefresh
? { skipSubtaskParentRefresh: true }
: {}),
});
if (!options.skipRelationRefresh)
await publishEvent("task-relation.refresh", {
projectId: after.projectId,
userId,
});
}
if (changed("title"))
await publishEvent("task.title_changed", {
...common,
oldTitle: before.title,
newTitle: after.title,
type: "title_changed",
});
if (changed("priority"))
await publishEvent("task.priority_changed", {
...common,
oldPriority: before.priority,
newPriority: after.priority,
type: "priority_changed",
});
if (changed("userId")) {
const assignee =
options.assigneeName !== undefined
? { name: options.assigneeName ?? undefined }
: after.userId
? (
await db
.select({ name: userTable.name })
.from(userTable)
.where(eq(userTable.id, after.userId))
.limit(1)
)[0]
: undefined;
await publishEvent(
after.userId ? "task.assignee_changed" : "task.unassigned",
{
...common,
oldAssignee: before.userId,
newAssignee: assignee?.name,
newAssigneeId: after.userId,
type: after.userId ? "assignee_changed" : "unassigned",
},
);
}
if (
(!options.fields || options.fields.includes("dueDate")) &&
before.dueDate?.getTime() !== after.dueDate?.getTime()
)
await publishEvent("task.due_date_changed", {
...common,
oldDueDate: before.dueDate,
newDueDate: after.dueDate,
type: "due_date_changed",
});
if (
(!options.fields || options.fields.includes("description")) &&
before.description !== undefined &&
before.description !== after.description
) {
await publishEvent("task.description_changed", {
...common,
oldDescription: before.description,
newDescription: after.description,
type: "description_changed",
});
deleteOrphanedAssets(before.description, after.description, {
taskId: after.id,
}).catch(() => {});
const oldMentions = new Set(parseMentionIds(before.description));
const newlyMentioned = parseMentionIds(after.description).filter(
(id) => id !== userId && !oldMentions.has(id),
);
if (newlyMentioned.length) {
const editor = userId
? (
await db
.select({ name: userTable.name })
.from(userTable)
.where(eq(userTable.id, userId))
.limit(1)
)[0]
: undefined;
for (const mentionedId of newlyMentioned)
await createNotification({
userId: mentionedId,
type: "task_mention",
eventData: {
taskTitle: after.title,
mentionerName: editor?.name ?? null,
},
resourceId: after.id,
resourceType: "task",
});
}
}
}
Loading