Files
archon/archon-ui-main/src/features/projects/tasks/components/TaskEditModal.tsx
Wirasm f4ad785439 refactor: Phase 2 Query Keys Standardization - Complete TanStack Query v5 patterns implementation (#692)
* refactor: complete Phase 2 Query Keys Standardization

Standardize query keys across all features following vertical slice architecture,
ensuring they mirror backend API structure exactly with no backward compatibility.

Key Changes:
- Refactor all query key factories to follow consistent patterns
- Move progress feature from knowledge/progress to top-level /features/progress
- Create shared query patterns for consistency (DISABLED_QUERY_KEY, STALE_TIMES)
- Remove all hardcoded stale times and disabled keys
- Update all imports after progress feature relocation

Query Key Factories Standardized:
- projectKeys: removed task-related keys (tasks, taskCounts)
- taskKeys: added dual nature support (global via lists(), project-scoped via byProject())
- knowledgeKeys: removed redundant methods (details, summary)
- progressKeys: new top-level feature with consistent factory
- documentKeys: full factory pattern with versions support
- mcpKeys: complete with health endpoint

Shared Patterns Implementation:
- STALE_TIMES: instant (0), realtime (3s), frequent (5s), normal (30s), rare (5m), static (∞)
- DISABLED_QUERY_KEY: consistent disabled query pattern across all features
- Removed unused createQueryOptions helper

Testing:
- Added comprehensive tests for progress hooks
- Updated all test mocks to include new STALE_TIMES values
- All 81 feature tests passing

Documentation:
- Created QUERY_PATTERNS.md guide for future implementations
- Clear patterns, examples, and migration checklist

Breaking Changes:
- Progress imports moved from knowledge/progress to progress
- Query key structure changes (cache will reset)
- No backward compatibility maintained

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: establish single source of truth for tags in metadata

- Remove ambiguous top-level tags field from KnowledgeItem interface
- Update all UI components to use metadata.tags exclusively
- Fix mutations to correctly update tags in metadata object
- Remove duplicate tags field from backend KnowledgeSummaryService
- Fix test setup issue with QueryClient instance in knowledge tests
- Add TODO comments for filter-blind optimistic updates (Phase 3)

This eliminates the ambiguity identified in Phase 2 where both item.tags
and metadata.tags existed, establishing metadata.tags as the single
source of truth across the entire stack.

* fix: comprehensive progress hooks improvements

- Integrate useSmartPolling for all polling queries
- Fix memory leaks from uncleaned timeouts
- Replace string-based error checking with status codes
- Remove TypeScript any usage with proper types
- Fix unstable dependencies with sorted JSON serialization
- Add staleTime to document queries for consistency

* feat: implement flexible assignee system for dynamic agents

- Changed assignee from restricted enum to flexible string type
- Renamed "AI IDE Agent" to "Coding Agent" for clarity
- Enhanced ComboBox with Radix UI best practices:
  - Full ARIA compliance (roles, labels, keyboard nav)
  - Performance optimizations (memoization, useCallback)
  - Improved UX (auto-scroll, keyboard shortcuts)
  - Fixed event bubbling preventing unintended modal opens
- Updated MCP server docs to reflect flexible assignee capability
- Removed unnecessary UI elements (arrows, helper text)
- Styled ComboBox to match priority selector aesthetic

This allows external MCP clients to create and assign custom sub-agents
dynamically, supporting advanced agent orchestration workflows.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: complete Phase 2 summariesPrefix usage for cache consistency

- Fix all knowledgeKeys.summaries() calls to use summariesPrefix() for operations targeting multiple summary caches
- Update cancelQueries, getQueriesData, setQueriesData, invalidateQueries, and refetchQueries calls
- Fix critical cache invalidation bug where filtered summaries weren't being cleared
- Update test expectations to match new factory patterns
- Address CodeRabbit review feedback on cache stability issues

This completes the Phase 2 Query Keys Standardization work documented in PRPs/local/frontend-state-management-refactor.md

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: update MCP task tools documentation for Coding Agent rename

Update task assignee documentation from "AI IDE Agent" to "Coding Agent"
to match frontend changes for consistency across the system.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: implement assignee filtering in MCP find_tasks function

Add missing implementation for filter_by="assignee" that was documented
but not coded. The filter now properly passes the assignee parameter to
the backend API, matching the existing pattern used for status filtering.

Fixes documentation/implementation mismatch identified by CodeRabbit.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Phase 2 cleanup - address review comments and improve code quality

Changes made:
- Reduced smart polling interval from 60s to 5s for background tabs (better responsiveness)
- Fixed cache coherence bug in knowledge queries (missing limit parameter)
- Standardized "Coding Agent" naming (was inconsistently "AI IDE Agent")
- Improved task queries with 2s polling, type safety, and proper invalidation
- Enhanced combobox accessibility with proper ARIA attributes and IDs
- Delegated useCrawlProgressPolling to useActiveOperations (removed duplication)
- Added exact: true to progress query removals (prevents sibling removal)
- Fixed invalid Tailwind class ml-4.5 to ml-4

All changes align with Phase 2 query key standardization goals and improve
overall code quality, accessibility, and performance.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 11:05:03 +03:00

210 lines
6.9 KiB
TypeScript

import { memo, useCallback, useEffect, useState } from "react";
import {
Button,
ComboBox,
type ComboBoxOption,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
FormField,
FormGrid,
Input,
Label,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
TextArea,
} from "../../../ui/primitives";
import { useTaskEditor } from "../hooks";
import { type Assignee, COMMON_ASSIGNEES, type Task, type TaskPriority } from "../types";
import { FeatureSelect } from "./FeatureSelect";
interface TaskEditModalProps {
isModalOpen: boolean;
editingTask: Task | null;
projectId: string;
onClose: () => void;
onSaved?: () => void;
onOpenChange?: (open: boolean) => void;
}
// Convert common assignees to ComboBox options
const ASSIGNEE_OPTIONS: ComboBoxOption[] = COMMON_ASSIGNEES.map((name) => ({
value: name,
label: name,
description:
name === "User" ? "Assign to human user" : name === "Archon" ? "Assign to Archon system" : "Assign to Coding Agent",
}));
export const TaskEditModal = memo(
({ isModalOpen, editingTask, projectId, onClose, onSaved, onOpenChange }: TaskEditModalProps) => {
const [localTask, setLocalTask] = useState<Partial<Task> | null>(null);
// Use business logic hook
const { projectFeatures, saveTask, isLoadingFeatures, isSaving: isSavingTask } = useTaskEditor(projectId);
// Sync local state with editingTask when it changes
useEffect(() => {
if (editingTask) {
setLocalTask(editingTask);
} else {
// Reset for new task
setLocalTask({
title: "",
description: "",
status: "todo",
assignee: "User" as Assignee,
feature: "",
priority: "medium" as TaskPriority, // Direct priority field
});
}
}, [editingTask]);
// Memoized handlers for input changes
const handleTitleChange = useCallback((value: string) => {
setLocalTask((prev) => (prev ? { ...prev, title: value } : null));
}, []);
const handleDescriptionChange = useCallback((value: string) => {
setLocalTask((prev) => (prev ? { ...prev, description: value } : null));
}, []);
const handleFeatureChange = useCallback((value: string) => {
setLocalTask((prev) => (prev ? { ...prev, feature: value } : null));
}, []);
const handleSave = useCallback(() => {
// All validation is now in the hook
saveTask(localTask, editingTask, () => {
onSaved?.();
onClose();
});
}, [localTask, editingTask, saveTask, onSaved, onClose]);
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
return (
<Dialog open={isModalOpen} onOpenChange={onOpenChange || ((open) => !open && onClose())}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingTask?.id ? "Edit Task" : "New Task"}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<FormField>
<Label required>Title</Label>
<Input
value={localTask?.title || ""}
onChange={(e) => handleTitleChange(e.target.value)}
placeholder="Enter task title"
/>
</FormField>
<FormField>
<Label>Description</Label>
<TextArea
value={localTask?.description || ""}
onChange={(e) => handleDescriptionChange(e.target.value)}
rows={5}
placeholder="Enter task description"
/>
</FormField>
<FormGrid columns={2}>
<FormField>
<Label>Status</Label>
<Select
value={localTask?.status || "todo"}
onValueChange={(value) =>
setLocalTask((prev) => (prev ? { ...prev, status: value as Task["status"] } : null))
}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todo">Todo</SelectItem>
<SelectItem value="doing">Doing</SelectItem>
<SelectItem value="review">Review</SelectItem>
<SelectItem value="done">Done</SelectItem>
</SelectContent>
</Select>
</FormField>
<FormField>
<Label>Priority</Label>
<Select
value={localTask?.priority || "medium"}
onValueChange={(value) =>
setLocalTask((prev) => (prev ? { ...prev, priority: value as TaskPriority } : null))
}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="critical">Critical</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
</FormField>
</FormGrid>
<FormGrid columns={2}>
<FormField>
<Label>Assignee</Label>
<ComboBox
options={ASSIGNEE_OPTIONS}
value={localTask?.assignee || "User"}
onValueChange={(value) => setLocalTask((prev) => (prev ? { ...prev, assignee: value } : null))}
placeholder="Select or type assignee..."
searchPlaceholder="Search or enter custom..."
emptyMessage="Type a custom assignee name"
className="w-full"
allowCustomValue={true}
/>
</FormField>
<FormField>
<Label>Feature</Label>
<FeatureSelect
value={localTask?.feature || ""}
onChange={handleFeatureChange}
projectFeatures={projectFeatures}
isLoadingFeatures={isLoadingFeatures}
placeholder="Select or create feature..."
className="w-full"
/>
</FormField>
</FormGrid>
</div>
<DialogFooter>
<Button onClick={handleClose} variant="outline" disabled={isSavingTask}>
Cancel
</Button>
<Button
onClick={handleSave}
variant="cyan"
loading={isSavingTask}
disabled={isSavingTask || !localTask?.title}
>
{editingTask?.id ? "Update Task" : "Create Task"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
},
);
TaskEditModal.displayName = "TaskEditModal";