Deleting user now works, and forces you to allocate tasks from each user to another user before deleting them
This commit is contained in:
@@ -70,6 +70,7 @@
|
|||||||
- **`Escape`**: Instantly close any open modal (Task Detail, Add Task, Settings, or Audit Logs).
|
- **`Escape`**: Instantly close any open modal (Task Detail, Add Task, Settings, or Audit Logs).
|
||||||
- **`n`**: Open the "Add Task" modal from the main dashboard (disabled while typing in inputs).
|
- **`n`**: Open the "Add Task" modal from the main dashboard (disabled while typing in inputs).
|
||||||
24. **Drag-and-Drop Stability:** Fixed a bug where tasks would "disappear" if dropped in an invalid area. Tasks now remain visible at their original position if a drop is cancelled.
|
24. **Drag-and-Drop Stability:** Fixed a bug where tasks would "disappear" if dropped in an invalid area. Tasks now remain visible at their original position if a drop is cancelled.
|
||||||
|
25. **User Deletion Safety:** Implemented a backend check to prevent deleting users who have assigned tasks or notes. Upgraded the frontend `ApiService` to correctly parse and display these descriptive error messages from the backend.
|
||||||
|
|
||||||
### Phase 3: Advanced Features
|
### Phase 3: Advanced Features
|
||||||
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ class ApiService {
|
|||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
this.logout();
|
this.logout();
|
||||||
}
|
}
|
||||||
throw new Error(`API Error: ${response.statusText}`);
|
let errorMsg = response.statusText;
|
||||||
|
try {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data && data.detail) errorMsg = data.detail;
|
||||||
|
} catch (e) {}
|
||||||
|
throw new Error(`API Error: ${errorMsg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json();
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ function App() {
|
|||||||
await api.addAudit({ actor: meId, action: 'user_deleted', summary: 'Removed ' + (u?u.name:id), target: null });
|
await api.addAudit({ actor: meId, action: 'user_deleted', summary: 'Removed ' + (u?u.name:id), target: null });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to delete user");
|
alert("Failed to delete user: " + e.message);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onUpdateUserRole={async (id, edits) => {
|
onUpdateUserRole={async (id, edits) => {
|
||||||
@@ -401,7 +401,7 @@ function App() {
|
|||||||
await api.addAudit({ actor: meId, action: 'user_updated', summary: 'Updated ' + (userMap[id]?userMap[id].name:id) + ' permissions', target: null });
|
await api.addAudit({ actor: meId, action: 'user_updated', summary: 'Updated ' + (userMap[id]?userMap[id].name:id) + ' permissions', target: null });
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to update user");
|
alert("Failed to update user: " + e.message);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChangePassword={async (oldPwd, newPwd) => {
|
onChangePassword={async (oldPwd, newPwd) => {
|
||||||
|
|||||||
@@ -97,6 +97,19 @@ def delete_user(user_id: str, db: Session = Depends(get_db), current_user: model
|
|||||||
if not db_user:
|
if not db_user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
# Check for OPEN assigned tasks (that are not in the trash)
|
||||||
|
open_tasks = db.query(models.Task).filter(
|
||||||
|
models.Task.assignee_id == user_id,
|
||||||
|
models.Task.status == "open",
|
||||||
|
models.Task.deleted_at == None
|
||||||
|
).first()
|
||||||
|
if open_tasks:
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot delete user: They still have OPEN tasks assigned to them. Reassign them first.")
|
||||||
|
|
||||||
|
# Nullify references in closed tasks and notes so we don't lose history
|
||||||
|
db.query(models.Task).filter(models.Task.assignee_id == user_id).update({"assignee_id": None})
|
||||||
|
db.query(models.TaskNote).filter(models.TaskNote.author_id == user_id).update({"author_id": None})
|
||||||
|
|
||||||
db.delete(db_user)
|
db.delete(db_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "User deleted"}
|
return {"message": "User deleted"}
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ class TaskNote(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
task_id = Column(String, ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False)
|
task_id = Column(String, ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False)
|
||||||
author_id = Column(String, ForeignKey("users.id"), nullable=False)
|
author_id = Column(String, ForeignKey("users.id"), nullable=True)
|
||||||
body = Column(String, nullable=False)
|
body = Column(String, nullable=False)
|
||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ class Tag(TagBase):
|
|||||||
class TaskBase(BaseModel):
|
class TaskBase(BaseModel):
|
||||||
title: str
|
title: str
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
assignee_id: str
|
assignee_id: Optional[str] = None
|
||||||
added_by: str
|
added_by: str
|
||||||
priority: str
|
priority: str
|
||||||
source: str
|
source: str
|
||||||
|
|||||||
Reference in New Issue
Block a user