refactor: enhance code comments for clarity and maintainability across multiple files
This commit is contained in:
@@ -1438,6 +1438,8 @@ async def _lead_effective_dependencies(
|
|||||||
*,
|
*,
|
||||||
update: _TaskUpdateInput,
|
update: _TaskUpdateInput,
|
||||||
) -> tuple[list[UUID], list[UUID]]:
|
) -> tuple[list[UUID], list[UUID]]:
|
||||||
|
# Use newly normalized dependency updates when supplied; otherwise fall back
|
||||||
|
# to the task's current dependencies for blocked-by evaluation.
|
||||||
normalized_deps: list[UUID] | None = None
|
normalized_deps: list[UUID] | None = None
|
||||||
if update.depends_on_task_ids is not None:
|
if update.depends_on_task_ids is not None:
|
||||||
if update.task.status == "done":
|
if update.task.status == "done":
|
||||||
@@ -1659,6 +1661,8 @@ async def _apply_non_lead_agent_task_rules(
|
|||||||
and update.actor.agent.board_id != update.task.board_id
|
and update.actor.agent.board_id != update.task.board_id
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||||
|
# Agents are limited to status/comment updates, and non-inbox status moves
|
||||||
|
# must pass dependency checks before they can proceed.
|
||||||
allowed_fields = {"status", "comment"}
|
allowed_fields = {"status", "comment"}
|
||||||
if (
|
if (
|
||||||
update.depends_on_task_ids is not None
|
update.depends_on_task_ids is not None
|
||||||
@@ -1732,6 +1736,8 @@ async def _apply_admin_task_rules(
|
|||||||
target_status = _required_status_value(
|
target_status = _required_status_value(
|
||||||
update.updates.get("status", update.task.status),
|
update.updates.get("status", update.task.status),
|
||||||
)
|
)
|
||||||
|
# Reset blocked tasks to inbox unless the task is already done and remains
|
||||||
|
# done, which is the explicit done-task exception.
|
||||||
if blocked_ids and not (update.task.status == "done" and target_status == "done"):
|
if blocked_ids and not (update.task.status == "done" and target_status == "done"):
|
||||||
update.task.status = "inbox"
|
update.task.status = "inbox"
|
||||||
update.task.assigned_agent_id = None
|
update.task.assigned_agent_id = None
|
||||||
@@ -1788,6 +1794,8 @@ async def _record_task_update_activity(
|
|||||||
actor_agent_id = (
|
actor_agent_id = (
|
||||||
update.actor.agent.id if update.actor.actor_type == "agent" and update.actor.agent else None
|
update.actor.agent.id if update.actor.actor_type == "agent" and update.actor.agent else None
|
||||||
)
|
)
|
||||||
|
# Record the task transition first, then reconcile dependents so any
|
||||||
|
# cascaded dependency effects are logged after the source change.
|
||||||
record_activity(
|
record_activity(
|
||||||
session,
|
session,
|
||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
@@ -1888,6 +1896,8 @@ async def _finalize_updated_task(
|
|||||||
update.task.updated_at = utcnow()
|
update.task.updated_at = utcnow()
|
||||||
|
|
||||||
status_raw = update.updates.get("status")
|
status_raw = update.updates.get("status")
|
||||||
|
# Entering review requires either a new comment or a valid recent one to
|
||||||
|
# ensure reviewers get context on readiness.
|
||||||
if status_raw == "review":
|
if status_raw == "review":
|
||||||
comment_text = (update.comment or "").strip()
|
comment_text = (update.comment or "").strip()
|
||||||
if not comment_text and not await has_valid_recent_comment(
|
if not comment_text and not await has_valid_recent_comment(
|
||||||
|
|||||||
@@ -149,6 +149,8 @@ async def build_board_snapshot(session: AsyncSession, board: Board) -> BoardSnap
|
|||||||
approval_ids=approval_ids,
|
approval_ids=approval_ids,
|
||||||
)
|
)
|
||||||
task_title_by_id = {task.id: task.title for task in tasks}
|
task_title_by_id = {task.id: task.title for task in tasks}
|
||||||
|
# Hydrate each approval with linked task metadata, falling back to legacy
|
||||||
|
# single-task fields so older rows still render complete approval cards.
|
||||||
approval_reads = [
|
approval_reads = [
|
||||||
_approval_to_read(
|
_approval_to_read(
|
||||||
approval,
|
approval,
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ async def accept_invite(
|
|||||||
session.add(member)
|
session.add(member)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
||||||
|
# For scoped invites, copy invite board-access rows onto the member at accept
|
||||||
|
# time so effective permissions survive invite lifecycle cleanup.
|
||||||
if not (invite.all_boards_read or invite.all_boards_write):
|
if not (invite.all_boards_read or invite.all_boards_write):
|
||||||
access_rows = list(
|
access_rows = list(
|
||||||
await session.exec(
|
await session.exec(
|
||||||
|
|||||||
@@ -164,7 +164,8 @@ async def validate_dependency_update(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure the dependency graph is acyclic after applying the update.
|
# Rebuild the board-wide graph and overlay the pending edit for this task so
|
||||||
|
# validation catches indirect cycles created through existing edges.
|
||||||
task_ids = list(
|
task_ids = list(
|
||||||
await session.exec(
|
await session.exec(
|
||||||
select(col(Task.id)).where(col(Task.board_id) == board_id),
|
select(col(Task.id)).where(col(Task.board_id) == board_id),
|
||||||
|
|||||||
@@ -63,3 +63,25 @@ async def test_delete_board_cleans_org_board_access_rows() -> None:
|
|||||||
assert "organization_invite_board_access" in deleted_table_names
|
assert "organization_invite_board_access" in deleted_table_names
|
||||||
assert board in session.deleted
|
assert board in session.deleted
|
||||||
assert session.committed == 1
|
assert session.committed == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_board_cleans_tag_assignments_before_tasks() -> None:
|
||||||
|
"""Deleting a board should remove task-tag links before deleting tasks."""
|
||||||
|
session: Any = _FakeSession(exec_results=[[], [uuid4()]])
|
||||||
|
board = Board(
|
||||||
|
id=uuid4(),
|
||||||
|
organization_id=uuid4(),
|
||||||
|
name="Demo Board",
|
||||||
|
slug="demo-board",
|
||||||
|
gateway_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
await boards.delete_board(
|
||||||
|
session=session,
|
||||||
|
board=board,
|
||||||
|
)
|
||||||
|
|
||||||
|
deleted_table_names = [statement.table.name for statement in session.executed]
|
||||||
|
assert "tag_assignments" in deleted_table_names
|
||||||
|
assert deleted_table_names.index("tag_assignments") < deleted_table_names.index("tasks")
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ export const TaskBoard = memo(function TaskBoard({
|
|||||||
return positions;
|
return positions;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Animate card reordering smoothly by applying FLIP whenever layout positions change.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const cardRefsSnapshot = cardRefs.current;
|
const cardRefsSnapshot = cardRefs.current;
|
||||||
if (animationRafRef.current !== null) {
|
if (animationRafRef.current !== null) {
|
||||||
@@ -275,6 +276,7 @@ export const TaskBoard = memo(function TaskBoard({
|
|||||||
return buckets;
|
return buckets;
|
||||||
}, [tasks]);
|
}, [tasks]);
|
||||||
|
|
||||||
|
// Keep drag/drop state and payload handling centralized for column move interactions.
|
||||||
const handleDragStart =
|
const handleDragStart =
|
||||||
(task: Task) => (event: React.DragEvent<HTMLDivElement>) => {
|
(task: Task) => (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (readOnly) {
|
if (readOnly) {
|
||||||
@@ -344,6 +346,7 @@ export const TaskBoard = memo(function TaskBoard({
|
|||||||
>
|
>
|
||||||
{columns.map((column) => {
|
{columns.map((column) => {
|
||||||
const columnTasks = grouped[column.status] ?? [];
|
const columnTasks = grouped[column.status] ?? [];
|
||||||
|
// Derive review tab counts and the active subset from one canonical task list.
|
||||||
const reviewCounts =
|
const reviewCounts =
|
||||||
column.status === "review"
|
column.status === "review"
|
||||||
? columnTasks.reduce(
|
? columnTasks.reduce(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type DropdownSelectProps = {
|
|||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve trigger placeholder text with explicit prop override first, then accessible fallback.
|
||||||
const resolvePlaceholder = (ariaLabel: string, placeholder?: string) => {
|
const resolvePlaceholder = (ariaLabel: string, placeholder?: string) => {
|
||||||
if (placeholder) {
|
if (placeholder) {
|
||||||
return placeholder;
|
return placeholder;
|
||||||
@@ -51,6 +52,7 @@ const resolvePlaceholder = (ariaLabel: string, placeholder?: string) => {
|
|||||||
return trimmed.endsWith("...") ? trimmed : `${trimmed}...`;
|
return trimmed.endsWith("...") ? trimmed : `${trimmed}...`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve search input placeholder from explicit override or a normalized aria label.
|
||||||
const resolveSearchPlaceholder = (
|
const resolveSearchPlaceholder = (
|
||||||
ariaLabel: string,
|
ariaLabel: string,
|
||||||
searchPlaceholder?: string,
|
searchPlaceholder?: string,
|
||||||
@@ -107,6 +109,7 @@ export default function DropdownSelect({
|
|||||||
handleOpenChange(false);
|
handleOpenChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reset list scroll when opening or refining search so results start at the top.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user