Add migration to fix subtasκ  ordering (#554)

* Add migration to fix subtasκ  ordering

* Fix test issues
This commit is contained in:
Chris 2025-11-17 12:09:31 +02:00 committed by GitHub
parent 1008130e2c
commit 61ef6d7ac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 123 additions and 23 deletions

View file

@ -0,0 +1,68 @@
'use strict';
const {
safeAddColumns,
safeAddIndex,
safeRemoveColumn,
} = require('../utils/migration-utils');
module.exports = {
async up(queryInterface, Sequelize) {
// Add order column to tasks table for subtask ordering
await safeAddColumns(queryInterface, 'tasks', [
{
name: 'order',
definition: {
type: Sequelize.INTEGER,
allowNull: true,
defaultValue: null,
comment: 'Order position for subtasks within a parent task',
},
},
]);
// Add index on parent_task_id and order for efficient subtask queries
await safeAddIndex(
queryInterface,
'tasks',
['parent_task_id', 'order'],
{
name: 'tasks_parent_task_id_order',
}
);
// Populate order field for existing subtasks based on created_at
await queryInterface.sequelize.query(`
UPDATE tasks
SET "order" = subquery.row_num
FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY parent_task_id
ORDER BY created_at ASC
) as row_num
FROM tasks
WHERE parent_task_id IS NOT NULL
) AS subquery
WHERE tasks.id = subquery.id
`);
},
async down(queryInterface) {
// Remove the index
const indexes = await queryInterface.showIndex('tasks');
const indexExists = indexes.some(
(index) => index.name === 'tasks_parent_task_id_order'
);
if (indexExists) {
await queryInterface.removeIndex(
'tasks',
'tasks_parent_task_id_order'
);
}
// Remove the order column using safe utility
await safeRemoveColumn(queryInterface, 'tasks', 'order');
},
};

View file

@ -139,6 +139,11 @@ module.exports = (sequelize) => {
key: 'id',
},
},
order: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Order position for subtasks within a parent task',
},
completed_at: {
type: DataTypes.DATE,
allowNull: true,
@ -162,6 +167,10 @@ module.exports = (sequelize) => {
{
fields: ['parent_task_id'],
},
{
name: 'tasks_parent_task_id_order',
fields: ['parent_task_id', 'order'],
},
],
}
);

View file

@ -35,7 +35,10 @@ async function getSubtasks(parentTaskId, userId, timezone) {
required: false,
},
],
order: [['created_at', 'ASC']],
order: [
['order', 'ASC'],
['created_at', 'ASC'],
], // Order by order field, fallback to created_at
}
);
@ -49,9 +52,16 @@ async function getSubtasks(parentTaskId, userId, timezone) {
async function createSubtasks(parentTaskId, subtasks, userId) {
if (!subtasks || !Array.isArray(subtasks)) return;
// Get the highest order value for existing subtasks
const existingSubtasks = await taskRepository.findAll(
{ parent_task_id: parentTaskId },
{ attributes: ['order'], order: [['order', 'DESC']], limit: 1 }
);
const maxOrder = existingSubtasks[0]?.order ?? 0;
const subtasksData = subtasks
.filter((subtask) => subtask.name && subtask.name.trim())
.map((subtask) => ({
.map((subtask, index) => ({
name: subtask.name.trim(),
parent_task_id: parentTaskId,
user_id: userId,
@ -66,6 +76,7 @@ async function createSubtasks(parentTaskId, subtasks, userId) {
today: subtask.today || false,
recurrence_type: 'none',
completion_based: false,
order: maxOrder + index + 1, // Assign sequential order values
}));
await taskRepository.createMany(subtasksData);
@ -90,36 +101,43 @@ async function updateSubtasks(taskId, subtasks, userId) {
});
}
// Update order for all subtasks to reflect their position in the array
const allSubtasksToUpdate = subtasks.filter((s) => s.id);
const subtasksToUpdate = subtasks.filter(
(s) =>
s.id &&
((s.isEdited && s.name && s.name.trim()) || s._statusChanged)
);
if (subtasksToUpdate.length > 0) {
const updatePromises = subtasksToUpdate.map((subtask) => {
const updateData = {};
if (subtasksToUpdate.length > 0 || allSubtasksToUpdate.length > 0) {
const updatePromises = allSubtasksToUpdate.map((subtask, index) => {
const updateData = {
order: index + 1, // Update order based on position in array
};
if (subtask.isEdited && subtask.name && subtask.name.trim()) {
updateData.name = subtask.name.trim();
}
if (subtask._statusChanged || subtask.status !== undefined) {
updateData.status = parseStatus(subtask.status);
if (
updateData.status === Task.STATUS.DONE &&
!subtask.completed_at
) {
updateData.completed_at = new Date();
} else if (updateData.status !== Task.STATUS.DONE) {
updateData.completed_at = null;
if (subtasksToUpdate.includes(subtask)) {
if (subtask.isEdited && subtask.name && subtask.name.trim()) {
updateData.name = subtask.name.trim();
}
}
if (subtask.priority !== undefined) {
updateData.priority =
parsePriority(subtask.priority) || Task.PRIORITY.LOW;
if (subtask._statusChanged || subtask.status !== undefined) {
updateData.status = parseStatus(subtask.status);
if (
updateData.status === Task.STATUS.DONE &&
!subtask.completed_at
) {
updateData.completed_at = new Date();
} else if (updateData.status !== Task.STATUS.DONE) {
updateData.completed_at = null;
}
}
if (subtask.priority !== undefined) {
updateData.priority =
parsePriority(subtask.priority) || Task.PRIORITY.LOW;
}
}
return taskRepository.bulkUpdate(updateData, {

View file

@ -26,6 +26,11 @@ const TASK_INCLUDES_WITH_SUBTASKS = [
through: { attributes: [] },
},
],
separate: true, // Required for order to work with associations
order: [
['order', 'ASC'],
['created_at', 'ASC'],
],
},
];