feat: decouple task priority from task order (#652)
* feat: decouple task priority from task order This implements a dedicated priority system that operates independently from the existing task_order system, allowing users to set task priority without affecting visual drag-and-drop positioning. ## Changes Made ### Database - Add priority column to archon_tasks table with enum type (critical, high, medium, low) - Create database migration with safe enum handling and data backfill - Add proper indexing for performance ### Backend - Update UpdateTaskRequest to include priority field - Add priority validation in TaskService with enum checking - Include priority field in task list responses and ETag generation - Fix cache invalidation for priority updates ### Frontend - Update TaskPriority type from "urgent" to "critical" for consistency - Add changePriority method to useTaskActions hook - Update TaskCard to use direct priority field instead of task_order conversion - Update TaskEditModal priority form to use direct priority values - Fix TaskPriorityComponent to use correct priority enum values - Update buildTaskUpdates to include priority field changes - Add priority field to Task interface as required field - Update test fixtures to include priority field ## Key Features - ✅ Users can change task priority without affecting drag-and-drop order - ✅ Users can drag tasks to reorder without changing priority level - ✅ Priority persists correctly in database with dedicated column - ✅ All existing priority functionality continues working identically - ✅ Cache invalidation works properly for priority changes - ✅ Both TaskCard priority button and TaskEditModal priority work 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add priority column to complete_setup.sql for fresh installations - Add task_priority enum type (low, medium, high, critical) - Add priority column to archon_tasks table with default 'medium' - Add index for priority column performance - Add documentation comment for priority field This ensures fresh installations include the priority system without needing to run the separate migration. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: include priority field in task creation payload When creating new tasks via TaskEditModal, the buildCreateRequest function was not including the priority field, causing new tasks to fall back to the database default ('medium') instead of respecting the user's selected priority in the modal. Added priority: localTask.priority || 'medium' to ensure the user's chosen priority is sent to the API during task creation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: make priority migration safe and idempotent Replaced destructive DROP TYPE CASCADE with safe migration patterns: - Use DO blocks with EXCEPTION handling for enum and column creation - Prevent conflicts with complete_setup.sql for fresh installations - Enhanced backfill logic to preserve user-modified priorities - Only update tasks that haven't been modified (updated_at = created_at) - Add comprehensive error handling with informative notices - Migration can now be run multiple times safely This ensures the migration works for both existing installations (incremental migration) and fresh installations (complete_setup.sql) without data loss or conflicts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: enforce NOT NULL constraint on priority column Data integrity improvements: Migration (add_priority_column_to_tasks.sql): - Add column as nullable first with DEFAULT 'medium' - Update any NULL values to 'medium' - Set NOT NULL constraint to enforce application invariants - Safe handling for existing columns with proper constraint checking Complete Setup (complete_setup.sql): - Priority column now DEFAULT 'medium' NOT NULL for fresh installations - Ensures consistency between migration and fresh install paths Both paths now enforce priority field as required, matching the frontend Task interface where priority is a required field. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add priority support to task creation API Complete priority support for task creation: API Routes (projects_api.py): - Add priority field to CreateTaskRequest Pydantic model - Pass request.priority to TaskService.create_task call Task Service (task_service.py): - Add priority parameter to create_task method signature - Add priority validation using existing validate_priority method - Include priority field in database INSERT task_data - Include priority field in API response task object This ensures that new tasks created via TaskEditModal respect the user's selected priority instead of falling back to database default. Validation ensures only valid priority values (low, medium, high, critical) are accepted and stored in the database. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: implement clean slate priority migration (no backward compatibility) Remove all task_order to priority mapping logic for true decoupling: - All existing tasks get 'medium' priority (clean slate) - No complex CASE logic or task_order relationships - Users explicitly set priorities as needed after migration - Truly independent priority and visual ordering systems - Simpler, safer migration with no coupling logic This approach prioritizes clean architecture over preserving implied user intentions from the old coupled system. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: rename TaskPriority.tsx to TaskPriorityComponent.tsx for consistency - Renamed file to match the exported component name - Updated import in index.ts barrel export - Maintains consistency with other component naming patterns --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Rasmus Widing <rasmus.widing@gmail.com>
This commit is contained in:
parent
9f2d70ae0e
commit
c45842f0bb
@ -1,13 +1,13 @@
|
||||
import { Tag } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useDrag, useDrop } from "react-dnd";
|
||||
import { useTaskActions } from "../hooks";
|
||||
import type { Assignee, Task } from "../types";
|
||||
import type { Assignee, Task, TaskPriority } from "../types";
|
||||
import { getOrderColor, getOrderGlow, ItemTypes } from "../utils/task-styles";
|
||||
import { TaskAssignee } from "./TaskAssignee";
|
||||
import { TaskCardActions } from "./TaskCardActions";
|
||||
import { type Priority, TaskPriority } from "./TaskPriority";
|
||||
import { TaskPriorityComponent } from ".";
|
||||
|
||||
export interface TaskCardProps {
|
||||
task: Task;
|
||||
@ -34,12 +34,8 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
selectedTasks,
|
||||
onTaskSelect,
|
||||
}) => {
|
||||
// Local state for frontend-only priority
|
||||
// NOTE: Priority is display-only and doesn't sync with backend yet
|
||||
const [localPriority, setLocalPriority] = useState<Priority>("medium");
|
||||
|
||||
// Use business logic hook
|
||||
const { changeAssignee, isUpdating } = useTaskActions(projectId);
|
||||
// Use business logic hook with changePriority
|
||||
const { changeAssignee, changePriority, isUpdating } = useTaskActions(projectId);
|
||||
|
||||
// Handlers - now just call hook methods
|
||||
const handleEdit = useCallback(() => {
|
||||
@ -59,10 +55,12 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
}
|
||||
}, [onDelete, task]);
|
||||
|
||||
const handlePriorityChange = useCallback((priority: Priority) => {
|
||||
// Frontend-only priority change
|
||||
setLocalPriority(priority);
|
||||
}, []);
|
||||
const handlePriorityChange = useCallback(
|
||||
(priority: TaskPriority) => {
|
||||
changePriority(task.id, priority);
|
||||
},
|
||||
[changePriority, task.id],
|
||||
);
|
||||
|
||||
const handleAssigneeChange = useCallback(
|
||||
(newAssignee: Assignee) => {
|
||||
@ -218,8 +216,12 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
||||
<div className="flex items-center justify-between mt-auto pt-2 pl-1.5 pr-3">
|
||||
<TaskAssignee assignee={task.assignee} onAssigneeChange={handleAssigneeChange} isLoading={isUpdating} />
|
||||
|
||||
{/* Priority display (frontend-only for now) */}
|
||||
<TaskPriority priority={localPriority} onPriorityChange={handlePriorityChange} isLoading={false} />
|
||||
{/* Priority display connected to database */}
|
||||
<TaskPriorityComponent
|
||||
priority={task.priority}
|
||||
onPriorityChange={handlePriorityChange}
|
||||
isLoading={isUpdating}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -18,9 +18,8 @@ import {
|
||||
TextArea,
|
||||
} from "../../../ui/primitives";
|
||||
import { useTaskEditor } from "../hooks";
|
||||
import type { Assignee, Task } from "../types";
|
||||
import type { Assignee, Task, TaskPriority } from "../types";
|
||||
import { FeatureSelect } from "./FeatureSelect";
|
||||
import type { Priority } from "./TaskPriority";
|
||||
|
||||
interface TaskEditModalProps {
|
||||
isModalOpen: boolean;
|
||||
@ -52,7 +51,7 @@ export const TaskEditModal = memo(
|
||||
status: "todo",
|
||||
assignee: "User" as Assignee,
|
||||
feature: "",
|
||||
priority: "medium" as Priority, // Frontend-only priority
|
||||
priority: "medium" as TaskPriority, // Direct priority field
|
||||
});
|
||||
}
|
||||
}, [editingTask]);
|
||||
@ -133,9 +132,9 @@ export const TaskEditModal = memo(
|
||||
<FormField>
|
||||
<Label>Priority</Label>
|
||||
<Select
|
||||
value={(localTask as Task & { priority?: Priority })?.priority || "medium"}
|
||||
value={localTask?.priority || "medium"}
|
||||
onValueChange={(value) =>
|
||||
setLocalTask((prev) => (prev ? { ...prev, priority: value as Priority } : null))
|
||||
setLocalTask((prev) => (prev ? { ...prev, priority: value as TaskPriority } : null))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
|
||||
@ -11,18 +11,17 @@ import { AlertCircle } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "../../../ui/primitives/select";
|
||||
import { cn, glassmorphism } from "../../../ui/primitives/styles";
|
||||
|
||||
export type Priority = "critical" | "high" | "medium" | "low";
|
||||
import type { TaskPriority } from "../types";
|
||||
|
||||
interface TaskPriorityProps {
|
||||
priority?: Priority;
|
||||
onPriorityChange?: (priority: Priority) => void;
|
||||
priority?: TaskPriority;
|
||||
onPriorityChange?: (priority: TaskPriority) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
// Priority options for the dropdown
|
||||
const PRIORITY_OPTIONS: Array<{
|
||||
value: Priority;
|
||||
value: TaskPriority;
|
||||
label: string;
|
||||
color: string;
|
||||
}> = [
|
||||
@ -32,13 +31,13 @@ const PRIORITY_OPTIONS: Array<{
|
||||
{ value: "low", label: "Low", color: "text-gray-600" },
|
||||
];
|
||||
|
||||
export const TaskPriority: React.FC<TaskPriorityProps> = ({
|
||||
export const TaskPriorityComponent: React.FC<TaskPriorityProps> = ({
|
||||
priority = "medium",
|
||||
onPriorityChange,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
// Get priority-specific styling with Tron glow
|
||||
const getPriorityStyles = (priorityValue: Priority) => {
|
||||
const getPriorityStyles = (priorityValue: TaskPriority) => {
|
||||
switch (priorityValue) {
|
||||
case "critical":
|
||||
return {
|
||||
@ -101,7 +100,7 @@ export const TaskPriority: React.FC<TaskPriorityProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Select value={priority} onValueChange={(value) => onPriorityChange(value as Priority)}>
|
||||
<Select value={priority} onValueChange={(value) => onPriorityChange(value as TaskPriority)}>
|
||||
<SelectTrigger
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
@ -13,4 +13,4 @@ export type { TaskCardProps } from "./TaskCard";
|
||||
export { TaskCard } from "./TaskCard";
|
||||
export { TaskCardActions } from "./TaskCardActions";
|
||||
export { TaskEditModal } from "./TaskEditModal";
|
||||
export { TaskPriority as TaskPriorityComponent } from "./TaskPriority";
|
||||
export { TaskPriorityComponent } from "./TaskPriorityComponent";
|
||||
|
||||
@ -69,6 +69,7 @@ describe("useTaskQueries", () => {
|
||||
status: "todo",
|
||||
assignee: "User",
|
||||
task_order: 100,
|
||||
priority: "medium",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
@ -120,6 +121,7 @@ describe("useTaskQueries", () => {
|
||||
status: "todo",
|
||||
assignee: "User",
|
||||
task_order: 100,
|
||||
priority: "medium",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
@ -159,6 +161,7 @@ describe("useTaskQueries", () => {
|
||||
status: "todo",
|
||||
assignee: "User",
|
||||
task_order: 100,
|
||||
priority: "medium",
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
updated_at: "2024-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { Assignee, Task, UseTaskActionsReturn } from "../types";
|
||||
import type { Assignee, Task, TaskPriority, UseTaskActionsReturn } from "../types";
|
||||
import { useDeleteTask, useUpdateTask } from "./useTaskQueries";
|
||||
|
||||
export const useTaskActions = (projectId: string): UseTaskActionsReturn => {
|
||||
@ -21,6 +21,17 @@ export const useTaskActions = (projectId: string): UseTaskActionsReturn => {
|
||||
[updateTaskMutation],
|
||||
);
|
||||
|
||||
// Priority change handler
|
||||
const changePriority = useCallback(
|
||||
(taskId: string, newPriority: TaskPriority) => {
|
||||
updateTaskMutation.mutate({
|
||||
taskId,
|
||||
updates: { priority: newPriority },
|
||||
});
|
||||
},
|
||||
[updateTaskMutation],
|
||||
);
|
||||
|
||||
// Delete task handler with confirmation flow - now accepts full task object
|
||||
const initiateDelete = useCallback((task: Task) => {
|
||||
setTaskToDelete(task);
|
||||
@ -54,6 +65,7 @@ export const useTaskActions = (projectId: string): UseTaskActionsReturn => {
|
||||
return {
|
||||
// Actions
|
||||
changeAssignee,
|
||||
changePriority,
|
||||
initiateDelete,
|
||||
confirmDelete,
|
||||
cancelDelete,
|
||||
|
||||
@ -37,6 +37,7 @@ export const useTaskEditor = (projectId: string): UseTaskEditorReturn => {
|
||||
if (localTask.status !== editingTask.status) updates.status = localTask.status;
|
||||
if (localTask.assignee !== editingTask.assignee) updates.assignee = localTask.assignee || "User";
|
||||
if (localTask.task_order !== editingTask.task_order) updates.task_order = localTask.task_order;
|
||||
if (localTask.priority !== editingTask.priority) updates.priority = localTask.priority;
|
||||
if (localTask.feature !== editingTask.feature) updates.feature = localTask.feature || "";
|
||||
|
||||
return updates;
|
||||
@ -51,6 +52,7 @@ export const useTaskEditor = (projectId: string): UseTaskEditorReturn => {
|
||||
description: localTask.description || "",
|
||||
status: (localTask.status as Task["status"]) || "todo",
|
||||
assignee: (localTask.assignee as Assignee) || "User",
|
||||
priority: localTask.priority || "medium",
|
||||
feature: localTask.feature || "",
|
||||
task_order: localTask.task_order || getDefaultTaskOrder((localTask.status as Task["status"]) || "todo"),
|
||||
};
|
||||
|
||||
@ -85,7 +85,10 @@ export const taskService = {
|
||||
});
|
||||
|
||||
// Invalidate related caches
|
||||
// Note: We don't know the project_id here, so TanStack Query will handle invalidation
|
||||
// Invalidate the specific project's tasks using the returned task data
|
||||
if (task.project_id) {
|
||||
invalidateETagCache(`/api/projects/${task.project_id}/tasks`);
|
||||
}
|
||||
invalidateETagCache("/api/tasks/counts");
|
||||
|
||||
return task;
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* Type definitions for task-related hooks
|
||||
*/
|
||||
|
||||
import type { Task } from "./task";
|
||||
import type { Task, TaskPriority } from "./task";
|
||||
|
||||
/**
|
||||
* Return type for useTaskActions hook
|
||||
@ -12,6 +12,7 @@ import type { Task } from "./task";
|
||||
export interface UseTaskActionsReturn {
|
||||
// Actions
|
||||
changeAssignee: (taskId: string, newAssignee: string) => void;
|
||||
changePriority: (taskId: string, newPriority: TaskPriority) => void;
|
||||
initiateDelete: (task: Task) => void;
|
||||
confirmDelete: () => void;
|
||||
cancelDelete: () => void;
|
||||
|
||||
@ -1,39 +1,36 @@
|
||||
/**
|
||||
* Priority System Types
|
||||
*
|
||||
* Defines user-facing priority levels separate from task_order (which handles drag-and-drop positioning).
|
||||
* Priority is for display and user understanding, not for ordering logic.
|
||||
* Defines priority levels independent from task_order (which handles drag-and-drop positioning).
|
||||
* Priority represents semantic importance and is stored directly in the database.
|
||||
*/
|
||||
|
||||
export type TaskPriority = "critical" | "high" | "medium" | "low";
|
||||
|
||||
export interface TaskPriorityOption {
|
||||
value: number; // Maps to task_order values for backwards compatibility
|
||||
value: TaskPriority; // Direct priority values from database enum
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const TASK_PRIORITY_OPTIONS: readonly TaskPriorityOption[] = [
|
||||
{ value: 1, label: "Critical", color: "text-red-600" },
|
||||
{ value: 25, label: "High", color: "text-orange-600" },
|
||||
{ value: 50, label: "Medium", color: "text-blue-600" },
|
||||
{ value: 100, label: "Low", color: "text-gray-600" },
|
||||
{ value: "critical", label: "Critical", color: "text-red-600" },
|
||||
{ value: "high", label: "High", color: "text-orange-600" },
|
||||
{ value: "medium", label: "Medium", color: "text-blue-600" },
|
||||
{ value: "low", label: "Low", color: "text-gray-600" },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Convert task_order value to TaskPriority enum
|
||||
* Get task priority display properties from priority value
|
||||
*/
|
||||
export function getTaskPriorityFromTaskOrder(taskOrder: number): TaskPriority {
|
||||
if (taskOrder <= 1) return "critical";
|
||||
if (taskOrder <= 25) return "high";
|
||||
if (taskOrder <= 50) return "medium";
|
||||
return "low";
|
||||
export function getTaskPriorityOption(priority: TaskPriority): TaskPriorityOption {
|
||||
const priorityOption = TASK_PRIORITY_OPTIONS.find((p) => p.value === priority);
|
||||
return priorityOption || TASK_PRIORITY_OPTIONS[2]; // Default to 'Medium'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get task priority display properties from task_order
|
||||
* Validate priority value against allowed enum values
|
||||
*/
|
||||
export function getTaskPriorityOption(taskOrder: number): TaskPriorityOption {
|
||||
const priority = TASK_PRIORITY_OPTIONS.find((p) => p.value >= taskOrder);
|
||||
return priority || TASK_PRIORITY_OPTIONS[TASK_PRIORITY_OPTIONS.length - 1]; // Default to 'Low'
|
||||
export function isValidTaskPriority(priority: string): priority is TaskPriority {
|
||||
return ["critical", "high", "medium", "low"].includes(priority);
|
||||
}
|
||||
|
||||
@ -59,9 +59,11 @@ export interface Task {
|
||||
archived_at?: string;
|
||||
archived_by?: string;
|
||||
|
||||
// Priority field (required database field)
|
||||
priority: TaskPriority;
|
||||
|
||||
// Extended UI properties
|
||||
featureColor?: string;
|
||||
priority?: TaskPriority;
|
||||
}
|
||||
|
||||
// Request types
|
||||
|
||||
107
migration/add_priority_column_to_tasks.sql
Normal file
107
migration/add_priority_column_to_tasks.sql
Normal file
@ -0,0 +1,107 @@
|
||||
-- =====================================================
|
||||
-- Add priority column to archon_tasks table
|
||||
-- =====================================================
|
||||
-- This migration adds a dedicated priority column to decouple
|
||||
-- task priority from task_order field:
|
||||
-- - priority: Enum field for semantic importance (low, medium, high, critical)
|
||||
-- - task_order: Remains for visual drag-and-drop positioning only
|
||||
--
|
||||
-- This solves the coupling issue where changing task position
|
||||
-- accidentally changed task priority, enabling independent
|
||||
-- priority management and visual task organization.
|
||||
--
|
||||
-- SAFE & IDEMPOTENT: Can be run multiple times without issues
|
||||
-- Compatible with complete_setup.sql for fresh installations
|
||||
-- =====================================================
|
||||
|
||||
-- Create enum type for task priority (safe, idempotent)
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE task_priority AS ENUM ('low', 'medium', 'high', 'critical');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN
|
||||
-- Type already exists, check if it has the right values
|
||||
RAISE NOTICE 'task_priority enum already exists, skipping creation';
|
||||
END $$;
|
||||
|
||||
-- Add priority column to archon_tasks table (safe, idempotent with NOT NULL constraint)
|
||||
DO $$ BEGIN
|
||||
-- Add column as nullable first with default
|
||||
ALTER TABLE archon_tasks ADD COLUMN priority task_priority DEFAULT 'medium';
|
||||
|
||||
-- Ensure all existing rows have the default value (handles any NULLs)
|
||||
UPDATE archon_tasks SET priority = 'medium' WHERE priority IS NULL;
|
||||
|
||||
-- Make column NOT NULL to enforce application invariants
|
||||
ALTER TABLE archon_tasks ALTER COLUMN priority SET NOT NULL;
|
||||
|
||||
RAISE NOTICE 'Added priority column with NOT NULL constraint and default value';
|
||||
EXCEPTION
|
||||
WHEN duplicate_column THEN
|
||||
-- Column exists, ensure it's NOT NULL and has proper default
|
||||
BEGIN
|
||||
-- Ensure no NULL values exist
|
||||
UPDATE archon_tasks SET priority = 'medium' WHERE priority IS NULL;
|
||||
|
||||
-- Ensure NOT NULL constraint (safe if already NOT NULL)
|
||||
BEGIN
|
||||
ALTER TABLE archon_tasks ALTER COLUMN priority SET NOT NULL;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE 'priority column already has NOT NULL constraint';
|
||||
END;
|
||||
|
||||
-- Ensure default value is set (safe if already set)
|
||||
BEGIN
|
||||
ALTER TABLE archon_tasks ALTER COLUMN priority SET DEFAULT 'medium';
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE NOTICE 'priority column already has default value';
|
||||
END;
|
||||
|
||||
END;
|
||||
RAISE NOTICE 'priority column already exists, ensured NOT NULL constraint and default';
|
||||
END $$;
|
||||
|
||||
-- Add index for the priority column for better query performance (safe, idempotent)
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_priority ON archon_tasks(priority);
|
||||
|
||||
-- Add comment to document the new column (safe, idempotent)
|
||||
DO $$ BEGIN
|
||||
COMMENT ON COLUMN archon_tasks.priority IS 'Task priority level independent of visual ordering - used for semantic importance (low, medium, high, critical)';
|
||||
EXCEPTION
|
||||
WHEN undefined_column THEN
|
||||
RAISE NOTICE 'priority column does not exist yet, skipping comment';
|
||||
END $$;
|
||||
|
||||
-- Set all existing tasks to default priority (clean slate approach)
|
||||
-- This truly decouples priority from task_order - no relationship at all
|
||||
DO $$
|
||||
DECLARE
|
||||
updated_count INTEGER;
|
||||
BEGIN
|
||||
-- Only proceed if priority column exists
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'archon_tasks' AND column_name = 'priority') THEN
|
||||
|
||||
-- Set all existing tasks to medium priority (clean slate)
|
||||
-- Users can explicitly set priorities as needed after migration
|
||||
UPDATE archon_tasks
|
||||
SET priority = 'medium'::task_priority
|
||||
WHERE priority IS NULL; -- Only update NULL values, preserve any existing priorities
|
||||
|
||||
GET DIAGNOSTICS updated_count = ROW_COUNT;
|
||||
RAISE NOTICE 'Set % existing tasks to medium priority (clean slate)', updated_count;
|
||||
ELSE
|
||||
RAISE NOTICE 'priority column does not exist, skipping initialization';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Note: After this migration, task_order and priority are completely independent:
|
||||
-- - task_order: Visual positioning in drag-and-drop operations only
|
||||
-- - priority: Semantic importance (critical, high, medium, low) only
|
||||
--
|
||||
-- Clean slate approach: All existing tasks start with 'medium' priority
|
||||
-- Users can explicitly set priorities as needed - no backward compatibility
|
||||
--
|
||||
-- This migration is safe to run multiple times and will not conflict
|
||||
-- with complete_setup.sql for fresh installations.
|
||||
@ -700,6 +700,13 @@ EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Create task_priority enum if it doesn't exist
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE task_priority AS ENUM ('low', 'medium', 'high', 'critical');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Assignee is now a text field to allow any agent name
|
||||
-- No longer using enum to support flexible agent assignments
|
||||
|
||||
@ -727,6 +734,7 @@ CREATE TABLE IF NOT EXISTS archon_tasks (
|
||||
status task_status DEFAULT 'todo',
|
||||
assignee TEXT DEFAULT 'User' CHECK (assignee IS NOT NULL AND assignee != ''),
|
||||
task_order INTEGER DEFAULT 0,
|
||||
priority task_priority DEFAULT 'medium' NOT NULL,
|
||||
feature TEXT,
|
||||
sources JSONB DEFAULT '[]'::jsonb,
|
||||
code_examples JSONB DEFAULT '[]'::jsonb,
|
||||
@ -776,6 +784,7 @@ CREATE INDEX IF NOT EXISTS idx_archon_tasks_project_id ON archon_tasks(project_i
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_status ON archon_tasks(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_assignee ON archon_tasks(assignee);
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_order ON archon_tasks(task_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_priority ON archon_tasks(priority);
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_archived ON archon_tasks(archived);
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_tasks_archived_at ON archon_tasks(archived_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_archon_project_sources_project_id ON archon_project_sources(project_id);
|
||||
@ -838,6 +847,7 @@ $$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add comments to document the soft delete fields
|
||||
COMMENT ON COLUMN archon_tasks.assignee IS 'The agent or user assigned to this task. Can be any valid agent name or "User"';
|
||||
COMMENT ON COLUMN archon_tasks.priority IS 'Task priority level independent of visual ordering - used for semantic importance (low, medium, high, critical)';
|
||||
COMMENT ON COLUMN archon_tasks.archived IS 'Soft delete flag - TRUE if task is archived/deleted';
|
||||
COMMENT ON COLUMN archon_tasks.archived_at IS 'Timestamp when task was archived';
|
||||
COMMENT ON COLUMN archon_tasks.archived_by IS 'User/system that archived the task';
|
||||
|
||||
@ -70,6 +70,7 @@ class CreateTaskRequest(BaseModel):
|
||||
status: str | None = "todo"
|
||||
assignee: str | None = "User"
|
||||
task_order: int | None = 0
|
||||
priority: str | None = "medium"
|
||||
feature: str | None = None
|
||||
|
||||
|
||||
@ -585,6 +586,7 @@ async def list_project_tasks(
|
||||
"status": task.get("status"),
|
||||
"task_order": task.get("task_order"),
|
||||
"assignee": task.get("assignee"),
|
||||
"priority": task.get("priority"),
|
||||
"feature": task.get("feature")
|
||||
} for task in tasks],
|
||||
"project_id": project_id,
|
||||
@ -634,6 +636,7 @@ async def create_task(request: CreateTaskRequest):
|
||||
description=request.description or "",
|
||||
assignee=request.assignee or "User",
|
||||
task_order=request.task_order or 0,
|
||||
priority=request.priority or "medium",
|
||||
feature=request.feature,
|
||||
)
|
||||
|
||||
@ -771,6 +774,7 @@ class UpdateTaskRequest(BaseModel):
|
||||
status: str | None = None
|
||||
assignee: str | None = None
|
||||
task_order: int | None = None
|
||||
priority: str | None = None
|
||||
feature: str | None = None
|
||||
|
||||
|
||||
@ -818,6 +822,8 @@ async def update_task(task_id: str, request: UpdateTaskRequest):
|
||||
update_fields["assignee"] = request.assignee
|
||||
if request.task_order is not None:
|
||||
update_fields["task_order"] = request.task_order
|
||||
if request.priority is not None:
|
||||
update_fields["priority"] = request.priority
|
||||
if request.feature is not None:
|
||||
update_fields["feature"] = request.feature
|
||||
|
||||
|
||||
@ -42,6 +42,16 @@ class TaskService:
|
||||
return False, "Assignee must be a non-empty string"
|
||||
return True, ""
|
||||
|
||||
def validate_priority(self, priority: str) -> tuple[bool, str]:
|
||||
"""Validate task priority against allowed enum values"""
|
||||
VALID_PRIORITIES = ["low", "medium", "high", "critical"]
|
||||
if priority not in VALID_PRIORITIES:
|
||||
return (
|
||||
False,
|
||||
f"Invalid priority '{priority}'. Must be one of: {', '.join(VALID_PRIORITIES)}",
|
||||
)
|
||||
return True, ""
|
||||
|
||||
async def create_task(
|
||||
self,
|
||||
project_id: str,
|
||||
@ -49,6 +59,7 @@ class TaskService:
|
||||
description: str = "",
|
||||
assignee: str = "User",
|
||||
task_order: int = 0,
|
||||
priority: str = "medium",
|
||||
feature: str | None = None,
|
||||
sources: list[dict[str, Any]] = None,
|
||||
code_examples: list[dict[str, Any]] = None,
|
||||
@ -72,6 +83,11 @@ class TaskService:
|
||||
if not is_valid:
|
||||
return False, {"error": error_msg}
|
||||
|
||||
# Validate priority
|
||||
is_valid, error_msg = self.validate_priority(priority)
|
||||
if not is_valid:
|
||||
return False, {"error": error_msg}
|
||||
|
||||
task_status = "todo"
|
||||
|
||||
# REORDERING LOGIC: If inserting at a specific position, increment existing tasks
|
||||
@ -104,6 +120,7 @@ class TaskService:
|
||||
"status": task_status,
|
||||
"assignee": assignee,
|
||||
"task_order": task_order,
|
||||
"priority": priority,
|
||||
"sources": sources or [],
|
||||
"code_examples": code_examples or [],
|
||||
"created_at": datetime.now().isoformat(),
|
||||
@ -128,6 +145,7 @@ class TaskService:
|
||||
"status": task["status"],
|
||||
"assignee": task["assignee"],
|
||||
"task_order": task["task_order"],
|
||||
"priority": task["priority"],
|
||||
"created_at": task["created_at"],
|
||||
}
|
||||
}
|
||||
@ -167,7 +185,7 @@ class TaskService:
|
||||
# Select all fields except large JSONB ones
|
||||
query = self.supabase_client.table("archon_tasks").select(
|
||||
"id, project_id, parent_task_id, title, description, "
|
||||
"status, assignee, task_order, feature, archived, "
|
||||
"status, assignee, task_order, priority, feature, archived, "
|
||||
"archived_at, archived_by, created_at, updated_at, "
|
||||
"sources, code_examples" # Still fetch for counting, but will process differently
|
||||
)
|
||||
@ -279,6 +297,7 @@ class TaskService:
|
||||
"status": task["status"],
|
||||
"assignee": task.get("assignee", "User"),
|
||||
"task_order": task.get("task_order", 0),
|
||||
"priority": task.get("priority", "medium"),
|
||||
"feature": task.get("feature"),
|
||||
"created_at": task["created_at"],
|
||||
"updated_at": task["updated_at"],
|
||||
@ -371,6 +390,12 @@ class TaskService:
|
||||
return False, {"error": error_msg}
|
||||
update_data["assignee"] = update_fields["assignee"]
|
||||
|
||||
if "priority" in update_fields:
|
||||
is_valid, error_msg = self.validate_priority(update_fields["priority"])
|
||||
if not is_valid:
|
||||
return False, {"error": error_msg}
|
||||
update_data["priority"] = update_fields["priority"]
|
||||
|
||||
if "task_order" in update_fields:
|
||||
update_data["task_order"] = update_fields["task_order"]
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user