feat(database): optimize task sorting functions and introduce bulk update capability
- Added new SQL migration to fix window function errors in task sorting functions, replacing CTEs with direct updates for better performance. - Introduced a bulk update function for task sort orders, allowing multiple updates in a single call to improve efficiency. - Updated socket command to support bulk updates, enhancing the task sorting experience in the frontend. - Simplified task update handling in the frontend to utilize the new bulk update feature, improving overall performance and user experience.
This commit is contained in:
@@ -0,0 +1,143 @@
|
|||||||
|
-- Fix window function error in task sort optimized functions
|
||||||
|
-- Error: window functions are not allowed in UPDATE
|
||||||
|
|
||||||
|
-- Replace the optimized sort functions to avoid CTE usage in UPDATE statements
|
||||||
|
CREATE OR REPLACE FUNCTION handle_task_list_sort_between_groups_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_offset INT := 0;
|
||||||
|
_affected_rows INT;
|
||||||
|
BEGIN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Use direct updates without CTE in UPDATE
|
||||||
|
IF (_to_index = -1)
|
||||||
|
THEN
|
||||||
|
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
|
||||||
|
IF _to_index > _from_index
|
||||||
|
THEN
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _from_index
|
||||||
|
AND sort_order < _to_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index - 1 WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _to_index < _from_index
|
||||||
|
THEN
|
||||||
|
_offset := 0;
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _to_index
|
||||||
|
AND sort_order < _from_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index + 1 WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Replace the second optimized sort function
|
||||||
|
CREATE OR REPLACE FUNCTION handle_task_list_sort_inside_group_optimized(_from_index integer, _to_index integer, _task_id uuid, _project_id uuid, _batch_size integer DEFAULT 100) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_offset INT := 0;
|
||||||
|
_affected_rows INT;
|
||||||
|
BEGIN
|
||||||
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
|
||||||
|
IF _to_index > _from_index
|
||||||
|
THEN
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order > _from_index
|
||||||
|
AND sort_order <= _to_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF _to_index < _from_index
|
||||||
|
THEN
|
||||||
|
_offset := 0;
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE project_id = _project_id
|
||||||
|
AND sort_order >= _to_index
|
||||||
|
AND sort_order < _from_index
|
||||||
|
AND sort_order > _offset
|
||||||
|
AND sort_order <= _offset + _batch_size;
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
|
EXIT WHEN _affected_rows = 0;
|
||||||
|
_offset := _offset + _batch_size;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Add simple bulk update function as alternative
|
||||||
|
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_update_record RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Simple approach: update each task's sort_order from the provided array
|
||||||
|
FOR _update_record IN
|
||||||
|
SELECT
|
||||||
|
(item->>'task_id')::uuid as task_id,
|
||||||
|
(item->>'sort_order')::int as sort_order,
|
||||||
|
(item->>'status_id')::uuid as status_id,
|
||||||
|
(item->>'priority_id')::uuid as priority_id,
|
||||||
|
(item->>'phase_id')::uuid as phase_id
|
||||||
|
FROM json_array_elements(_updates) as item
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET
|
||||||
|
sort_order = _update_record.sort_order,
|
||||||
|
status_id = COALESCE(_update_record.status_id, status_id),
|
||||||
|
priority_id = COALESCE(_update_record.priority_id, priority_id)
|
||||||
|
WHERE id = _update_record.task_id;
|
||||||
|
|
||||||
|
-- Handle phase updates separately since it's in a different table
|
||||||
|
IF _update_record.phase_id IS NOT NULL THEN
|
||||||
|
INSERT INTO task_phase (task_id, phase_id)
|
||||||
|
VALUES (_update_record.task_id, _update_record.phase_id)
|
||||||
|
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
@@ -5498,6 +5498,7 @@ DECLARE
|
|||||||
_iterator NUMERIC := 0;
|
_iterator NUMERIC := 0;
|
||||||
_status_id TEXT;
|
_status_id TEXT;
|
||||||
_project_id UUID;
|
_project_id UUID;
|
||||||
|
_base_sort_order NUMERIC;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- Get the project_id from the first status to ensure we update all statuses in the same project
|
-- Get the project_id from the first status to ensure we update all statuses in the same project
|
||||||
SELECT project_id INTO _project_id
|
SELECT project_id INTO _project_id
|
||||||
@@ -5513,17 +5514,28 @@ BEGIN
|
|||||||
_iterator := _iterator + 1;
|
_iterator := _iterator + 1;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
|
|
||||||
-- Ensure any remaining statuses in the project (not in the provided list) get sequential sort_order
|
-- Get the base sort order for remaining statuses (simple count approach)
|
||||||
-- This handles edge cases where not all statuses are provided
|
SELECT COUNT(*) INTO _base_sort_order
|
||||||
UPDATE task_statuses
|
|
||||||
SET sort_order = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM task_statuses ts2
|
FROM task_statuses ts2
|
||||||
WHERE ts2.project_id = _project_id
|
WHERE ts2.project_id = _project_id
|
||||||
AND ts2.id = ANY(SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID)
|
AND ts2.id = ANY(SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID);
|
||||||
) + ROW_NUMBER() OVER (ORDER BY sort_order) - 1
|
|
||||||
|
-- Update remaining statuses with simple sequential numbering
|
||||||
|
-- Reset iterator to start from base_sort_order
|
||||||
|
_iterator := _base_sort_order;
|
||||||
|
|
||||||
|
-- Use a cursor approach to avoid window functions
|
||||||
|
FOR _status_id IN
|
||||||
|
SELECT id::TEXT FROM task_statuses
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND id NOT IN (SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID);
|
AND id NOT IN (SELECT (TRIM(BOTH '"' FROM JSON_ARRAY_ELEMENTS(_status_ids)::TEXT))::UUID)
|
||||||
|
ORDER BY sort_order
|
||||||
|
LOOP
|
||||||
|
UPDATE task_statuses
|
||||||
|
SET sort_order = _iterator
|
||||||
|
WHERE id = _status_id::UUID;
|
||||||
|
_iterator := _iterator + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
RETURN;
|
RETURN;
|
||||||
END
|
END
|
||||||
@@ -6412,7 +6424,7 @@ DECLARE
|
|||||||
_offset INT := 0;
|
_offset INT := 0;
|
||||||
_affected_rows INT;
|
_affected_rows INT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- PERFORMANCE OPTIMIZATION: Use CTE for better query planning
|
-- PERFORMANCE OPTIMIZATION: Use direct updates without CTE in UPDATE
|
||||||
IF (_to_index = -1)
|
IF (_to_index = -1)
|
||||||
THEN
|
THEN
|
||||||
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
_to_index = COALESCE((SELECT MAX(sort_order) + 1 FROM tasks WHERE project_id = _project_id), 0);
|
||||||
@@ -6422,18 +6434,15 @@ BEGIN
|
|||||||
IF _to_index > _from_index
|
IF _to_index > _from_index
|
||||||
THEN
|
THEN
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order - 1
|
SET sort_order = sort_order - 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order > _from_index
|
AND sort_order > _from_index
|
||||||
AND sort_order < _to_index
|
AND sort_order < _to_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6445,18 +6454,15 @@ BEGIN
|
|||||||
THEN
|
THEN
|
||||||
_offset := 0;
|
_offset := 0;
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order + 1
|
SET sort_order = sort_order + 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order > _to_index
|
AND sort_order > _to_index
|
||||||
AND sort_order < _from_index
|
AND sort_order < _from_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6475,22 +6481,19 @@ DECLARE
|
|||||||
_offset INT := 0;
|
_offset INT := 0;
|
||||||
_affected_rows INT;
|
_affected_rows INT;
|
||||||
BEGIN
|
BEGIN
|
||||||
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets
|
-- PERFORMANCE OPTIMIZATION: Batch updates for large datasets without CTE in UPDATE
|
||||||
IF _to_index > _from_index
|
IF _to_index > _from_index
|
||||||
THEN
|
THEN
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order - 1
|
SET sort_order = sort_order - 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order > _from_index
|
AND sort_order > _from_index
|
||||||
AND sort_order <= _to_index
|
AND sort_order <= _to_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6500,18 +6503,15 @@ BEGIN
|
|||||||
THEN
|
THEN
|
||||||
_offset := 0;
|
_offset := 0;
|
||||||
LOOP
|
LOOP
|
||||||
WITH batch_update AS (
|
|
||||||
UPDATE tasks
|
UPDATE tasks
|
||||||
SET sort_order = sort_order + 1
|
SET sort_order = sort_order + 1
|
||||||
WHERE project_id = _project_id
|
WHERE project_id = _project_id
|
||||||
AND sort_order >= _to_index
|
AND sort_order >= _to_index
|
||||||
AND sort_order < _from_index
|
AND sort_order < _from_index
|
||||||
AND sort_order > _offset
|
AND sort_order > _offset
|
||||||
AND sort_order <= _offset + _batch_size
|
AND sort_order <= _offset + _batch_size;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
SELECT COUNT(*) INTO _affected_rows FROM batch_update;
|
|
||||||
|
|
||||||
|
GET DIAGNOSTICS _affected_rows = ROW_COUNT;
|
||||||
EXIT WHEN _affected_rows = 0;
|
EXIT WHEN _affected_rows = 0;
|
||||||
_offset := _offset + _batch_size;
|
_offset := _offset + _batch_size;
|
||||||
END LOOP;
|
END LOOP;
|
||||||
@@ -6520,3 +6520,38 @@ BEGIN
|
|||||||
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
UPDATE tasks SET sort_order = _to_index WHERE id = _task_id AND project_id = _project_id;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
-- Simple function to update task sort orders in bulk
|
||||||
|
CREATE OR REPLACE FUNCTION update_task_sort_orders_bulk(_updates json) RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
_update_record RECORD;
|
||||||
|
BEGIN
|
||||||
|
-- Simple approach: update each task's sort_order from the provided array
|
||||||
|
FOR _update_record IN
|
||||||
|
SELECT
|
||||||
|
(item->>'task_id')::uuid as task_id,
|
||||||
|
(item->>'sort_order')::int as sort_order,
|
||||||
|
(item->>'status_id')::uuid as status_id,
|
||||||
|
(item->>'priority_id')::uuid as priority_id,
|
||||||
|
(item->>'phase_id')::uuid as phase_id
|
||||||
|
FROM json_array_elements(_updates) as item
|
||||||
|
LOOP
|
||||||
|
UPDATE tasks
|
||||||
|
SET
|
||||||
|
sort_order = _update_record.sort_order,
|
||||||
|
status_id = COALESCE(_update_record.status_id, status_id),
|
||||||
|
priority_id = COALESCE(_update_record.priority_id, priority_id)
|
||||||
|
WHERE id = _update_record.task_id;
|
||||||
|
|
||||||
|
-- Handle phase updates separately since it's in a different table
|
||||||
|
IF _update_record.phase_id IS NOT NULL THEN
|
||||||
|
INSERT INTO task_phase (task_id, phase_id)
|
||||||
|
VALUES (_update_record.task_id, _update_record.phase_id)
|
||||||
|
ON CONFLICT (task_id) DO UPDATE SET phase_id = _update_record.phase_id;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ interface ChangeRequest {
|
|||||||
priority: string;
|
priority: string;
|
||||||
};
|
};
|
||||||
team_id: string;
|
team_id: string;
|
||||||
|
// New simplified approach
|
||||||
|
task_updates?: Array<{
|
||||||
|
task_id: string;
|
||||||
|
sort_order: number;
|
||||||
|
status_id?: string;
|
||||||
|
priority_id?: string;
|
||||||
|
phase_id?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
@@ -64,6 +72,38 @@ function updateUnmappedStatus(config: Config) {
|
|||||||
|
|
||||||
export async function on_task_sort_order_change(_io: Server, socket: Socket, data: ChangeRequest) {
|
export async function on_task_sort_order_change(_io: Server, socket: Socket, data: ChangeRequest) {
|
||||||
try {
|
try {
|
||||||
|
// New simplified approach - use bulk updates if provided
|
||||||
|
if (data.task_updates && data.task_updates.length > 0) {
|
||||||
|
// Check dependencies for status changes
|
||||||
|
if (data.group_by === GroupBy.STATUS && data.to_group) {
|
||||||
|
const canContinue = await TasksControllerV2.checkForCompletedDependencies(data.task.id, data.to_group);
|
||||||
|
if (!canContinue) {
|
||||||
|
return socket.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
|
completed_deps: canContinue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the simple bulk update function
|
||||||
|
const q = `SELECT update_task_sort_orders_bulk($1);`;
|
||||||
|
await db.query(q, [JSON.stringify(data.task_updates)]);
|
||||||
|
await emitSortOrderChange(data, socket);
|
||||||
|
|
||||||
|
// Handle notifications and logging
|
||||||
|
if (data.group_by === GroupBy.STATUS && data.to_group) {
|
||||||
|
notifyStatusChange(socket, {
|
||||||
|
task_id: data.task.id,
|
||||||
|
to_group: data.to_group,
|
||||||
|
from_group: data.from_group,
|
||||||
|
from_index: data.from_index,
|
||||||
|
to_index: data.to_index,
|
||||||
|
project_id: data.project_id,
|
||||||
|
group_by: data.group_by,
|
||||||
|
to_last_index: data.to_last_index
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to old complex method
|
||||||
const q = `SELECT handle_task_list_sort_order_change($1);`;
|
const q = `SELECT handle_task_list_sort_order_change($1);`;
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
@@ -94,8 +134,10 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
|
|||||||
|
|
||||||
await db.query(q, [JSON.stringify(config)]);
|
await db.query(q, [JSON.stringify(config)]);
|
||||||
await emitSortOrderChange(data, socket);
|
await emitSortOrderChange(data, socket);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.group_by === GroupBy.STATUS) {
|
// Common post-processing logic for both approaches
|
||||||
|
if (data.group_by === GroupBy.STATUS) {
|
||||||
const userId = getLoggedInUserIdFromSocket(socket);
|
const userId = getLoggedInUserIdFromSocket(socket);
|
||||||
const isAlreadyAssigned = await TasksControllerV2.checkUserAssignedToTask(data.task.id, userId as string, data.team_id);
|
const isAlreadyAssigned = await TasksControllerV2.checkUserAssignedToTask(data.task.id, userId as string, data.team_id);
|
||||||
|
|
||||||
@@ -104,7 +146,7 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.group_by === GroupBy.PHASE) {
|
if (data.group_by === GroupBy.PHASE) {
|
||||||
void logPhaseChange({
|
void logPhaseChange({
|
||||||
task_id: data.task.id,
|
task_id: data.task.id,
|
||||||
socket,
|
socket,
|
||||||
@@ -113,7 +155,7 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.group_by === GroupBy.STATUS) {
|
if (data.group_by === GroupBy.STATUS) {
|
||||||
void logStatusChange({
|
void logStatusChange({
|
||||||
task_id: data.task.id,
|
task_id: data.task.id,
|
||||||
socket,
|
socket,
|
||||||
@@ -122,7 +164,7 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.group_by === GroupBy.PRIORITY) {
|
if (data.group_by === GroupBy.PRIORITY) {
|
||||||
void logPriorityChange({
|
void logPriorityChange({
|
||||||
task_id: data.task.id,
|
task_id: data.task.id,
|
||||||
socket,
|
socket,
|
||||||
@@ -131,7 +173,7 @@ export async function on_task_sort_order_change(_io: Server, socket: Socket, dat
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void notifyProjectUpdates(socket, config.task_id);
|
void notifyProjectUpdates(socket, data.task.id);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log_error(error);
|
log_error(error);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { IKanbanTaskStatus } from '@/types/tasks/taskStatus.types';
|
|||||||
import { Modal as AntModal } from 'antd';
|
import { Modal as AntModal } from 'antd';
|
||||||
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
import { fetchTasksV3 } from '@/features/task-management/task-management.slice';
|
||||||
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
import { fetchEnhancedKanbanGroups } from '@/features/enhanced-kanban/enhanced-kanban.slice';
|
||||||
import { fetchTaskGroups } from '@/features/tasks/tasks.slice';
|
|
||||||
import './ManageStatusModal.css';
|
import './ManageStatusModal.css';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
@@ -594,7 +593,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
|||||||
// Refresh from server to ensure consistency
|
// Refresh from server to ensure consistency
|
||||||
dispatch(fetchStatuses(finalProjectId));
|
dispatch(fetchStatuses(finalProjectId));
|
||||||
dispatch(fetchTasksV3(finalProjectId));
|
dispatch(fetchTasksV3(finalProjectId));
|
||||||
dispatch(fetchTaskGroups(finalProjectId));
|
|
||||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error changing status category:', error);
|
console.error('Error changing status category:', error);
|
||||||
@@ -736,7 +734,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
|||||||
statusApiService.updateStatusOrder(requestBody, finalProjectId).then(() => {
|
statusApiService.updateStatusOrder(requestBody, finalProjectId).then(() => {
|
||||||
// Refresh task lists after status order change
|
// Refresh task lists after status order change
|
||||||
dispatch(fetchTasksV3(finalProjectId));
|
dispatch(fetchTasksV3(finalProjectId));
|
||||||
dispatch(fetchTaskGroups(finalProjectId));
|
|
||||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error updating status order:', error);
|
console.error('Error updating status order:', error);
|
||||||
@@ -767,7 +764,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
|||||||
if (res.done) {
|
if (res.done) {
|
||||||
dispatch(fetchStatuses(finalProjectId));
|
dispatch(fetchStatuses(finalProjectId));
|
||||||
dispatch(fetchTasksV3(finalProjectId));
|
dispatch(fetchTasksV3(finalProjectId));
|
||||||
dispatch(fetchTaskGroups(finalProjectId));
|
|
||||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -791,7 +787,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
|||||||
await statusApiService.updateNameOfStatus(id, body, finalProjectId);
|
await statusApiService.updateNameOfStatus(id, body, finalProjectId);
|
||||||
dispatch(fetchStatuses(finalProjectId));
|
dispatch(fetchStatuses(finalProjectId));
|
||||||
dispatch(fetchTasksV3(finalProjectId));
|
dispatch(fetchTasksV3(finalProjectId));
|
||||||
dispatch(fetchTaskGroups(finalProjectId));
|
|
||||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error renaming status:', error);
|
console.error('Error renaming status:', error);
|
||||||
@@ -813,7 +808,6 @@ const ManageStatusModal: React.FC<ManageStatusModalProps> = ({
|
|||||||
await statusApiService.deleteStatus(id, finalProjectId, replacingStatusId);
|
await statusApiService.deleteStatus(id, finalProjectId, replacingStatusId);
|
||||||
dispatch(fetchStatuses(finalProjectId));
|
dispatch(fetchStatuses(finalProjectId));
|
||||||
dispatch(fetchTasksV3(finalProjectId));
|
dispatch(fetchTasksV3(finalProjectId));
|
||||||
dispatch(fetchTaskGroups(finalProjectId));
|
|
||||||
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
dispatch(fetchEnhancedKanbanGroups(finalProjectId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting status:', error);
|
console.error('Error deleting status:', error);
|
||||||
|
|||||||
@@ -524,6 +524,69 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NEW SIMPLIFIED APPROACH: Calculate all affected task updates and send them
|
||||||
|
const taskUpdates: Array<{
|
||||||
|
task_id: string;
|
||||||
|
sort_order: number;
|
||||||
|
status_id?: string;
|
||||||
|
priority_id?: string;
|
||||||
|
phase_id?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Add updates for all tasks in affected groups
|
||||||
|
if (activeGroupId === overGroupId) {
|
||||||
|
// Same group - just reorder
|
||||||
|
const updatedTasks = [...sourceGroup.tasks];
|
||||||
|
updatedTasks.splice(fromIndex, 1);
|
||||||
|
updatedTasks.splice(toIndex, 0, task);
|
||||||
|
|
||||||
|
updatedTasks.forEach((task, index) => {
|
||||||
|
taskUpdates.push({
|
||||||
|
task_id: task.id,
|
||||||
|
sort_order: index + 1, // 1-based indexing
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Different groups - update both source and target
|
||||||
|
const updatedSourceTasks = sourceGroup.tasks.filter((_, i) => i !== fromIndex);
|
||||||
|
const updatedTargetTasks = [...targetGroup.tasks];
|
||||||
|
|
||||||
|
if (isTargetGroupEmpty) {
|
||||||
|
updatedTargetTasks.push(task);
|
||||||
|
} else if (toIndex >= 0 && toIndex <= updatedTargetTasks.length) {
|
||||||
|
updatedTargetTasks.splice(toIndex, 0, task);
|
||||||
|
} else {
|
||||||
|
updatedTargetTasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add updates for source group
|
||||||
|
updatedSourceTasks.forEach((task, index) => {
|
||||||
|
taskUpdates.push({
|
||||||
|
task_id: task.id,
|
||||||
|
sort_order: index + 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add updates for target group (including the moved task)
|
||||||
|
updatedTargetTasks.forEach((task, index) => {
|
||||||
|
const update: any = {
|
||||||
|
task_id: task.id,
|
||||||
|
sort_order: index + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add group-specific updates
|
||||||
|
if (groupBy === IGroupBy.STATUS) {
|
||||||
|
update.status_id = targetGroup.id;
|
||||||
|
} else if (groupBy === IGroupBy.PRIORITY) {
|
||||||
|
update.priority_id = targetGroup.id;
|
||||||
|
} else if (groupBy === IGroupBy.PHASE) {
|
||||||
|
update.phase_id = targetGroup.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
taskUpdates.push(update);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
socket?.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
socket?.emit(SocketEvents.TASK_SORT_ORDER_CHANGE.toString(), {
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
from_index: sourceGroup.tasks[fromIndex].sort_order,
|
from_index: sourceGroup.tasks[fromIndex].sort_order,
|
||||||
@@ -534,6 +597,7 @@ const TaskGroupWrapper = ({ taskGroups, groupBy }: TaskGroupWrapperProps) => {
|
|||||||
group_by: groupBy,
|
group_by: groupBy,
|
||||||
task: sourceGroup.tasks[fromIndex],
|
task: sourceGroup.tasks[fromIndex],
|
||||||
team_id: currentSession?.team_id,
|
team_id: currentSession?.team_id,
|
||||||
|
task_updates: taskUpdates, // NEW: Send calculated updates
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(resetTaskRowStyles, 0);
|
setTimeout(resetTaskRowStyles, 0);
|
||||||
|
|||||||
@@ -208,6 +208,18 @@ const TaskListTableWrapper = ({
|
|||||||
>
|
>
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<Flex style={{ transform: 'translateY(6px)' }}>
|
<Flex style={{ transform: 'translateY(6px)' }}>
|
||||||
|
{groupBy !== IGroupBy.PRIORITY &&
|
||||||
|
!showRenameInput &&
|
||||||
|
isEditable &&
|
||||||
|
name !== 'Unmapped' && (
|
||||||
|
<Dropdown menu={{ items }}>
|
||||||
|
<Button
|
||||||
|
icon={<EllipsisOutlined />}
|
||||||
|
className="borderless-icon-btn"
|
||||||
|
title={isEditable ? undefined : t('noPermission')}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="custom-collapse-button"
|
className="custom-collapse-button"
|
||||||
style={{
|
style={{
|
||||||
@@ -243,18 +255,6 @@ const TaskListTableWrapper = ({
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
{groupBy !== IGroupBy.PRIORITY &&
|
|
||||||
!showRenameInput &&
|
|
||||||
isEditable &&
|
|
||||||
name !== 'Unmapped' && (
|
|
||||||
<Dropdown menu={{ items }}>
|
|
||||||
<Button
|
|
||||||
icon={<EllipsisOutlined />}
|
|
||||||
className="borderless-icon-btn"
|
|
||||||
title={isEditable ? undefined : t('noPermission')}
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
isOpen={isExpanded}
|
isOpen={isExpanded}
|
||||||
|
|||||||
Reference in New Issue
Block a user