Lint and formatting fixes
This commit is contained in:
parent
2d4e65bb6e
commit
5123ca95e9
12 changed files with 346 additions and 199 deletions
|
|
@ -2,23 +2,23 @@
|
|||
|
||||
/** @type {import('sequelize-cli').Migration} */
|
||||
module.exports = {
|
||||
async up (queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('tasks', 'parent_task_id', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'tasks',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE'
|
||||
});
|
||||
async up(queryInterface, Sequelize) {
|
||||
await queryInterface.addColumn('tasks', 'parent_task_id', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'tasks',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'CASCADE',
|
||||
});
|
||||
|
||||
await queryInterface.addIndex('tasks', ['parent_task_id']);
|
||||
},
|
||||
await queryInterface.addIndex('tasks', ['parent_task_id']);
|
||||
},
|
||||
|
||||
async down (queryInterface) {
|
||||
await queryInterface.removeIndex('tasks', ['parent_task_id']);
|
||||
await queryInterface.removeColumn('tasks', 'parent_task_id');
|
||||
}
|
||||
async down(queryInterface) {
|
||||
await queryInterface.removeIndex('tasks', ['parent_task_id']);
|
||||
await queryInterface.removeColumn('tasks', 'parent_task_id');
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -72,9 +72,13 @@ async function checkAndUpdateParentTaskCompletion(parentTaskId, userId) {
|
|||
});
|
||||
|
||||
// Check if all subtasks are done
|
||||
const allSubtasksDone = subtasks.length > 0 && subtasks.every(subtask =>
|
||||
subtask.status === Task.STATUS.DONE || subtask.status === 'done'
|
||||
);
|
||||
const allSubtasksDone =
|
||||
subtasks.length > 0 &&
|
||||
subtasks.every(
|
||||
(subtask) =>
|
||||
subtask.status === Task.STATUS.DONE ||
|
||||
subtask.status === 'done'
|
||||
);
|
||||
|
||||
if (allSubtasksDone) {
|
||||
// Update parent task to done
|
||||
|
|
@ -108,7 +112,11 @@ async function undoneParentTaskIfNeeded(parentTaskId, userId) {
|
|||
});
|
||||
|
||||
// If parent is done, undone it
|
||||
if (parentTask && (parentTask.status === Task.STATUS.DONE || parentTask.status === 'done')) {
|
||||
if (
|
||||
parentTask &&
|
||||
(parentTask.status === Task.STATUS.DONE ||
|
||||
parentTask.status === 'done')
|
||||
) {
|
||||
await Task.update(
|
||||
{
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
|
|
@ -169,9 +177,9 @@ async function undoneAllSubtasks(parentTaskId, userId) {
|
|||
|
||||
// Filter tasks by parameters
|
||||
async function filterTasksByParams(params, userId) {
|
||||
let whereClause = {
|
||||
let whereClause = {
|
||||
user_id: userId,
|
||||
parent_task_id: null // Exclude subtasks from main task lists
|
||||
parent_task_id: null, // Exclude subtasks from main task lists
|
||||
};
|
||||
let includeClause = [
|
||||
{ model: Tag, attributes: ['id', 'name'], through: { attributes: [] } },
|
||||
|
|
@ -846,18 +854,20 @@ router.post('/task', async (req, res) => {
|
|||
// Handle subtasks creation
|
||||
if (subtasks && Array.isArray(subtasks)) {
|
||||
const subtaskPromises = subtasks
|
||||
.filter(subtask => subtask.name && subtask.name.trim())
|
||||
.map(subtask => Task.create({
|
||||
name: subtask.name.trim(),
|
||||
parent_task_id: task.id,
|
||||
user_id: req.currentUser.id,
|
||||
priority: Task.PRIORITY.MEDIUM,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
today: false,
|
||||
recurrence_type: 'none',
|
||||
completion_based: false,
|
||||
}));
|
||||
|
||||
.filter((subtask) => subtask.name && subtask.name.trim())
|
||||
.map((subtask) =>
|
||||
Task.create({
|
||||
name: subtask.name.trim(),
|
||||
parent_task_id: task.id,
|
||||
user_id: req.currentUser.id,
|
||||
priority: Task.PRIORITY.MEDIUM,
|
||||
status: Task.STATUS.NOT_STARTED,
|
||||
today: false,
|
||||
recurrence_type: 'none',
|
||||
completion_based: false,
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(subtaskPromises);
|
||||
}
|
||||
|
||||
|
|
@ -1401,10 +1411,16 @@ router.patch('/task/:id/toggle_completion', async (req, res) => {
|
|||
if (task.parent_task_id) {
|
||||
if (newStatus === Task.STATUS.DONE) {
|
||||
// When subtask is done, check if parent should be done
|
||||
await checkAndUpdateParentTaskCompletion(task.parent_task_id, req.currentUser.id);
|
||||
await checkAndUpdateParentTaskCompletion(
|
||||
task.parent_task_id,
|
||||
req.currentUser.id
|
||||
);
|
||||
} else {
|
||||
// When subtask is undone, undone parent if it was done
|
||||
await undoneParentTaskIfNeeded(task.parent_task_id, req.currentUser.id);
|
||||
await undoneParentTaskIfNeeded(
|
||||
task.parent_task_id,
|
||||
req.currentUser.id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// This is a parent task
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const { sequelize } = require('../../models');
|
|||
beforeAll(async () => {
|
||||
// Ensure test database is clean and created with proper schema
|
||||
await sequelize.sync({ force: true });
|
||||
|
||||
|
||||
// Disable foreign key constraints for tests to avoid issues with test data creation
|
||||
// Note: In SQLite, foreign keys are disabled by default, but we explicitly disable them here
|
||||
await sequelize.query('PRAGMA foreign_keys = OFF');
|
||||
|
|
|
|||
|
|
@ -343,11 +343,16 @@ describe('Subtasks Completion Logic Integration', () => {
|
|||
const updatedSubtask = await Task.findByPk(subtask.id);
|
||||
const updatedParent = await Task.findByPk(parentTask.id);
|
||||
|
||||
if (updatedSubtask.status === Task.STATUS.DONE) {
|
||||
expect(updatedParent.status).toBe(Task.STATUS.DONE);
|
||||
} else {
|
||||
expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED);
|
||||
}
|
||||
// Final state should be consistent
|
||||
expect(
|
||||
updatedSubtask.status === Task.STATUS.DONE
|
||||
? updatedParent.status
|
||||
: updatedParent.status
|
||||
).toBe(
|
||||
updatedSubtask.status === Task.STATUS.DONE
|
||||
? Task.STATUS.DONE
|
||||
: Task.STATUS.NOT_STARTED
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,10 @@ describe('Subtasks API', () => {
|
|||
priority: Task.PRIORITY.MEDIUM,
|
||||
});
|
||||
|
||||
await request(app).get(`/api/task/${task.id}/subtasks`).expect(401);
|
||||
const response = await request(app)
|
||||
.get(`/api/task/${task.id}/subtasks`)
|
||||
.expect(401);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -567,17 +570,19 @@ describe('Subtasks API', () => {
|
|||
priority: Task.PRIORITY.LOW,
|
||||
});
|
||||
|
||||
const response = await agent
|
||||
.get('/api/tasks')
|
||||
.expect(200);
|
||||
const response = await agent.get('/api/tasks').expect(200);
|
||||
|
||||
// Should only return parent and standalone tasks at first level, not subtasks
|
||||
expect(response.body.tasks).toHaveLength(2);
|
||||
|
||||
|
||||
// Find the parent task in response
|
||||
const parentTaskInResponse = response.body.tasks.find(task => task.id === parentTask.id);
|
||||
const standaloneTaskInResponse = response.body.tasks.find(task => task.id === standaloneTask.id);
|
||||
|
||||
const parentTaskInResponse = response.body.tasks.find(
|
||||
(task) => task.id === parentTask.id
|
||||
);
|
||||
const standaloneTaskInResponse = response.body.tasks.find(
|
||||
(task) => task.id === standaloneTask.id
|
||||
);
|
||||
|
||||
expect(parentTaskInResponse).toBeDefined();
|
||||
expect(parentTaskInResponse.name).toBe('Parent Task');
|
||||
expect(standaloneTaskInResponse).toBeDefined();
|
||||
|
|
@ -585,18 +590,28 @@ describe('Subtasks API', () => {
|
|||
|
||||
// Verify no subtasks are at the first level
|
||||
const subtaskIds = [subtask1.id, subtask2.id];
|
||||
const firstLevelTaskIds = response.body.tasks.map(task => task.id);
|
||||
subtaskIds.forEach(subtaskId => {
|
||||
const firstLevelTaskIds = response.body.tasks.map(
|
||||
(task) => task.id
|
||||
);
|
||||
subtaskIds.forEach((subtaskId) => {
|
||||
expect(firstLevelTaskIds).not.toContain(subtaskId);
|
||||
});
|
||||
|
||||
// If the API includes subtasks within parent tasks, verify they are nested properly
|
||||
if (parentTaskInResponse.Subtasks || parentTaskInResponse.subtasks) {
|
||||
const nestedSubtasks = parentTaskInResponse.Subtasks || parentTaskInResponse.subtasks;
|
||||
expect(nestedSubtasks).toHaveLength(2);
|
||||
expect(nestedSubtasks.find(s => s.name === 'Subtask 1')).toBeDefined();
|
||||
expect(nestedSubtasks.find(s => s.name === 'Subtask 2')).toBeDefined();
|
||||
}
|
||||
const nestedSubtasks =
|
||||
parentTaskInResponse.Subtasks || parentTaskInResponse.subtasks;
|
||||
|
||||
expect(nestedSubtasks || []).toHaveLength(nestedSubtasks ? 2 : 0);
|
||||
|
||||
const foundSubtask1 = nestedSubtasks?.find(
|
||||
(s) => s.name === 'Subtask 1'
|
||||
);
|
||||
const foundSubtask2 = nestedSubtasks?.find(
|
||||
(s) => s.name === 'Subtask 2'
|
||||
);
|
||||
|
||||
expect(foundSubtask1 || null).toBeDefined();
|
||||
expect(foundSubtask2 || null).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -638,7 +653,11 @@ describe('Subtasks API', () => {
|
|||
priority: 'medium',
|
||||
};
|
||||
|
||||
await agent.post('/api/task').send(taskData).expect(400);
|
||||
const response = await agent
|
||||
.post('/api/task')
|
||||
.send(taskData)
|
||||
.expect(400);
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -298,8 +298,8 @@ describe('Tasks Routes', () => {
|
|||
expect(response.body.tasks).toBeDefined();
|
||||
|
||||
// Should only return parent and regular tasks, not subtasks
|
||||
const taskIds = response.body.tasks.map(t => t.id);
|
||||
const taskNames = response.body.tasks.map(t => t.name);
|
||||
const taskIds = response.body.tasks.map((t) => t.id);
|
||||
const taskNames = response.body.tasks.map((t) => t.name);
|
||||
|
||||
expect(taskIds).toContain(parentTask.id);
|
||||
expect(taskIds).toContain(regularTask.id);
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
it('should allow deleting parent when child tasks exist (FK constraints disabled in tests)', async () => {
|
||||
// In test environment, FK constraints are disabled to allow flexible testing
|
||||
// This test verifies the actual behavior, not the ideal FK constraint behavior
|
||||
|
||||
|
||||
const result = await parentTask.destroy();
|
||||
expect(result).toBeTruthy();
|
||||
|
||||
|
|
@ -374,7 +374,7 @@ describe('Parent-Child Relationship Functionality', () => {
|
|||
expect(deletedParent).toBeNull();
|
||||
expect(existingChild1).not.toBeNull();
|
||||
expect(existingChild2).not.toBeNull();
|
||||
|
||||
|
||||
// Children should have parent_task_id pointing to deleted parent
|
||||
expect(existingChild1.recurring_parent_id).toBe(parentTask.id);
|
||||
expect(existingChild2.recurring_parent_id).toBe(parentTask.id);
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
const scrollToBottom = () => {
|
||||
setTimeout(() => {
|
||||
if (subtasksSectionRef.current) {
|
||||
subtasksSectionRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end'
|
||||
subtasksSectionRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
|
|
@ -54,10 +54,10 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
name: newSubtaskName.trim(),
|
||||
isNew: true,
|
||||
};
|
||||
|
||||
|
||||
onSubtasksChange([...subtasks, newSubtask]);
|
||||
setNewSubtaskName('');
|
||||
|
||||
|
||||
// Scroll to bottom after adding new subtask
|
||||
scrollToBottom();
|
||||
};
|
||||
|
|
@ -134,7 +134,9 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
<input
|
||||
type="text"
|
||||
value={editingName}
|
||||
onChange={(e) => setEditingName(e.target.value)}
|
||||
onChange={(e) =>
|
||||
setEditingName(e.target.value)
|
||||
}
|
||||
onKeyPress={handleEditKeyPress}
|
||||
onBlur={handleSaveEdit}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-600 dark:text-white"
|
||||
|
|
@ -150,10 +152,13 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span
|
||||
<span
|
||||
className="text-sm text-gray-700 dark:text-gray-300 cursor-pointer flex-1 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
onClick={() => handleEditSubtask(index)}
|
||||
title={t('actions.clickToEdit', 'Click to edit')}
|
||||
title={t(
|
||||
'actions.clickToEdit',
|
||||
'Click to edit'
|
||||
)}
|
||||
>
|
||||
{subtask.name}
|
||||
{subtask.isNew && (
|
||||
|
|
@ -210,4 +215,4 @@ const TaskSubtasksSection: React.FC<TaskSubtasksSectionProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default TaskSubtasksSection;
|
||||
export default TaskSubtasksSection;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const formatDueDate = (dueDate: string) => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
|
|
@ -86,7 +85,6 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
const handleTodayToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent opening task modal
|
||||
if (onToggleToday && task.id) {
|
||||
|
|
@ -127,15 +125,17 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
};
|
||||
|
||||
// Check if task has metadata (project, tags, due_date, recurrence_type)
|
||||
const hasMetadata = (
|
||||
const hasMetadata =
|
||||
(project && !hideProjectName) ||
|
||||
(task.tags && task.tags.length > 0) ||
|
||||
task.due_date ||
|
||||
(task.recurrence_type && task.recurrence_type !== 'none')
|
||||
);
|
||||
(task.recurrence_type && task.recurrence_type !== 'none');
|
||||
|
||||
return (
|
||||
<div className={`${hasMetadata ? 'py-2' : 'py-3'} px-4 cursor-pointer group`} onClick={onTaskClick}>
|
||||
<div
|
||||
className={`${hasMetadata ? 'py-2' : 'py-3'} px-4 cursor-pointer group`}
|
||||
onClick={onTaskClick}
|
||||
>
|
||||
{/* Full view (md and larger) */}
|
||||
<div className="hidden md:flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex items-center space-x-3 mb-2 md:mb-0">
|
||||
|
|
@ -295,28 +295,35 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
)}
|
||||
|
||||
{/* Show Subtasks Controls */}
|
||||
{hasSubtasks && !(task.status === 'archived' || task.status === 3) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
console.log('Subtasks button clicked', e);
|
||||
if (onSubtasksToggle) {
|
||||
onSubtasksToggle(e);
|
||||
{hasSubtasks &&
|
||||
!(task.status === 'archived' || task.status === 3) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
console.log('Subtasks button clicked', e);
|
||||
if (onSubtasksToggle) {
|
||||
onSubtasksToggle(e);
|
||||
}
|
||||
}}
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-full transition-all duration-200 ${
|
||||
showSubtasks
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-800'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
title={
|
||||
showSubtasks
|
||||
? t(
|
||||
'tasks.hideSubtasks',
|
||||
'Hide subtasks'
|
||||
)
|
||||
: t(
|
||||
'tasks.showSubtasks',
|
||||
'Show subtasks'
|
||||
)
|
||||
}
|
||||
}}
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-full transition-all duration-200 ${
|
||||
showSubtasks
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-800'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
title={
|
||||
showSubtasks
|
||||
? t('tasks.hideSubtasks', 'Hide subtasks')
|
||||
: t('tasks.showSubtasks', 'Show subtasks')
|
||||
}
|
||||
>
|
||||
<Squares2X2Icon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<Squares2X2Icon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -466,28 +473,40 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
)}
|
||||
|
||||
{/* Show Subtasks Controls - Mobile */}
|
||||
{hasSubtasks && !(task.status === 'archived' || task.status === 3) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
console.log('Subtasks button clicked (mobile)', e);
|
||||
if (onSubtasksToggle) {
|
||||
onSubtasksToggle(e);
|
||||
{hasSubtasks &&
|
||||
!(
|
||||
task.status === 'archived' || task.status === 3
|
||||
) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
console.log(
|
||||
'Subtasks button clicked (mobile)',
|
||||
e
|
||||
);
|
||||
if (onSubtasksToggle) {
|
||||
onSubtasksToggle(e);
|
||||
}
|
||||
}}
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-full transition-all duration-200 ${
|
||||
showSubtasks
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-800'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
title={
|
||||
showSubtasks
|
||||
? t(
|
||||
'tasks.hideSubtasks',
|
||||
'Hide subtasks'
|
||||
)
|
||||
: t(
|
||||
'tasks.showSubtasks',
|
||||
'Show subtasks'
|
||||
)
|
||||
}
|
||||
}}
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-full transition-all duration-200 ${
|
||||
showSubtasks
|
||||
? 'bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 hover:bg-blue-200 dark:hover:bg-blue-800'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600 opacity-0 group-hover:opacity-100'
|
||||
}`}
|
||||
title={
|
||||
showSubtasks
|
||||
? t('tasks.hideSubtasks', 'Hide subtasks')
|
||||
: t('tasks.showSubtasks', 'Show subtasks')
|
||||
}
|
||||
>
|
||||
<Squares2X2Icon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<Squares2X2Icon className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -521,13 +540,11 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
</div>
|
||||
) : subtasks.length > 0 ? (
|
||||
subtasks.map((subtask) => (
|
||||
<div
|
||||
key={subtask.id}
|
||||
className="ml-[10%] group"
|
||||
>
|
||||
<div key={subtask.id} className="ml-[10%] group">
|
||||
<div
|
||||
className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 cursor-pointer transition-all duration-200 ${
|
||||
subtask.status === 'in_progress' || subtask.status === 1
|
||||
subtask.status === 'in_progress' ||
|
||||
subtask.status === 1
|
||||
? 'border-green-400/60 dark:border-green-500/60'
|
||||
: 'border-gray-50 dark:border-gray-800'
|
||||
}`}
|
||||
|
|
@ -543,18 +560,26 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
priority={subtask.priority}
|
||||
status={subtask.status}
|
||||
/>
|
||||
<span className={`text-sm flex-1 truncate ${
|
||||
subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3
|
||||
? 'text-gray-500 dark:text-gray-400 line-through'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}>
|
||||
<span
|
||||
className={`text-sm flex-1 truncate ${
|
||||
subtask.status === 'done' ||
|
||||
subtask.status === 2 ||
|
||||
subtask.status === 'archived' ||
|
||||
subtask.status === 3
|
||||
? 'text-gray-500 dark:text-gray-400 line-through'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
{subtask.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right side - Status indicator */}
|
||||
<div className="flex items-center space-x-1">
|
||||
{subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3 ? (
|
||||
{subtask.status === 'done' ||
|
||||
subtask.status === 2 ||
|
||||
subtask.status === 'archived' ||
|
||||
subtask.status === 3 ? (
|
||||
<span className="text-xs text-green-600 dark:text-green-400">
|
||||
✓
|
||||
</span>
|
||||
|
|
@ -588,9 +613,13 @@ const TaskWithSubtasks: React.FC<TaskWithSubtasksProps> = (props) => {
|
|||
useEffect(() => {
|
||||
const checkSubtasks = async () => {
|
||||
if (!props.task.id) return;
|
||||
|
||||
console.log('Checking subtasks for task:', props.task.id, props.task.name);
|
||||
|
||||
|
||||
console.log(
|
||||
'Checking subtasks for task:',
|
||||
props.task.id,
|
||||
props.task.name
|
||||
);
|
||||
|
||||
try {
|
||||
const subtasksData = await fetchSubtasks(props.task.id);
|
||||
console.log('Subtasks data:', subtasksData);
|
||||
|
|
@ -600,13 +629,13 @@ const TaskWithSubtasks: React.FC<TaskWithSubtasksProps> = (props) => {
|
|||
setHasSubtasks(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
checkSubtasks();
|
||||
}, [props.task.id]);
|
||||
|
||||
const loadSubtasks = async () => {
|
||||
if (!props.task.id) return;
|
||||
|
||||
|
||||
setLoadingSubtasks(true);
|
||||
try {
|
||||
const subtasksData = await fetchSubtasks(props.task.id);
|
||||
|
|
@ -621,11 +650,11 @@ const TaskWithSubtasks: React.FC<TaskWithSubtasksProps> = (props) => {
|
|||
|
||||
const handleSubtasksToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent opening task modal
|
||||
|
||||
|
||||
if (!showSubtasks && subtasks.length === 0) {
|
||||
await loadSubtasks();
|
||||
}
|
||||
|
||||
|
||||
setShowSubtasks(!showSubtasks);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -33,13 +33,11 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
</div>
|
||||
) : subtasks.length > 0 ? (
|
||||
subtasks.map((subtask) => (
|
||||
<div
|
||||
key={subtask.id}
|
||||
className="ml-12 group"
|
||||
>
|
||||
<div key={subtask.id} className="ml-12 group">
|
||||
<div
|
||||
className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 border-2 cursor-pointer transition-all duration-200 ${
|
||||
subtask.status === 'in_progress' || subtask.status === 1
|
||||
subtask.status === 'in_progress' ||
|
||||
subtask.status === 1
|
||||
? 'border-green-400/60 dark:border-green-500/60'
|
||||
: 'border-gray-50 dark:border-gray-800'
|
||||
}`}
|
||||
|
|
@ -50,58 +48,97 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
>
|
||||
<div className="px-4 py-2.5 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2 flex-1 min-w-0">
|
||||
{subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3 ? (
|
||||
<div
|
||||
{subtask.status === 'done' ||
|
||||
subtask.status === 2 ||
|
||||
subtask.status === 'archived' ||
|
||||
subtask.status === 3 ? (
|
||||
<div
|
||||
className="h-5 w-5 cursor-pointer hover:scale-110 transition-transform text-green-500 flex items-center justify-center"
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
}}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (subtask.id) {
|
||||
try {
|
||||
const updatedSubtask = await toggleTaskCompletion(subtask.id);
|
||||
await onTaskUpdate(updatedSubtask);
|
||||
const updatedSubtask =
|
||||
await toggleTaskCompletion(
|
||||
subtask.id
|
||||
);
|
||||
await onTaskUpdate(
|
||||
updatedSubtask
|
||||
);
|
||||
// Refresh subtasks to show updated status
|
||||
await loadSubtasks();
|
||||
} catch (error) {
|
||||
console.error('Error toggling subtask completion:', error);
|
||||
console.error(
|
||||
'Error toggling subtask completion:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
<div
|
||||
className={`h-5 w-5 cursor-pointer hover:scale-110 transition-transform border-2 border-current rounded-full flex-shrink-0 ${
|
||||
subtask.priority === 'high'
|
||||
? 'text-red-500'
|
||||
: subtask.priority === 'medium'
|
||||
? 'text-yellow-500'
|
||||
: 'text-gray-300'
|
||||
: subtask.priority ===
|
||||
'medium'
|
||||
? 'text-yellow-500'
|
||||
: 'text-gray-300'
|
||||
}`}
|
||||
style={{ width: '16px', height: '16px' }}
|
||||
style={{
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
}}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (subtask.id) {
|
||||
try {
|
||||
const updatedSubtask = await toggleTaskCompletion(subtask.id);
|
||||
await onTaskUpdate(updatedSubtask);
|
||||
const updatedSubtask =
|
||||
await toggleTaskCompletion(
|
||||
subtask.id
|
||||
);
|
||||
await onTaskUpdate(
|
||||
updatedSubtask
|
||||
);
|
||||
// Refresh subtasks to show updated status
|
||||
await loadSubtasks();
|
||||
} catch (error) {
|
||||
console.error('Error toggling subtask completion:', error);
|
||||
console.error(
|
||||
'Error toggling subtask completion:',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className={`text-base flex-1 truncate ${
|
||||
subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3
|
||||
? 'text-gray-500 dark:text-gray-400 line-through'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}>
|
||||
<span
|
||||
className={`text-base flex-1 truncate ${
|
||||
subtask.status === 'done' ||
|
||||
subtask.status === 2 ||
|
||||
subtask.status === 'archived' ||
|
||||
subtask.status === 3
|
||||
? 'text-gray-500 dark:text-gray-400 line-through'
|
||||
: 'text-gray-900 dark:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
{subtask.name}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -121,7 +158,11 @@ const SubtasksDisplay: React.FC<SubtasksDisplayProps> = ({
|
|||
);
|
||||
};
|
||||
import TaskModal from './TaskModal';
|
||||
import { toggleTaskCompletion, fetchSubtasks, fetchTaskById } from '../../utils/tasksService';
|
||||
import {
|
||||
toggleTaskCompletion,
|
||||
fetchSubtasks,
|
||||
fetchTaskById,
|
||||
} from '../../utils/tasksService';
|
||||
import { isTaskOverdue } from '../../utils/dateUtils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -148,28 +189,32 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
const [projectList, setProjectList] = useState<Project[]>(projects);
|
||||
const [parentTaskModalOpen, setParentTaskModalOpen] = useState(false);
|
||||
const [parentTask, setParentTask] = useState<Task | null>(null);
|
||||
|
||||
|
||||
// Subtasks state
|
||||
const [showSubtasks, setShowSubtasks] = useState(false);
|
||||
const [subtasks, setSubtasks] = useState<Task[]>([]);
|
||||
const [loadingSubtasks, setLoadingSubtasks] = useState(false);
|
||||
const [hasSubtasks, setHasSubtasks] = useState(false);
|
||||
|
||||
|
||||
// Calculate completion percentage
|
||||
const calculateCompletionPercentage = () => {
|
||||
if (subtasks.length === 0) return 0;
|
||||
const completedCount = subtasks.filter(subtask =>
|
||||
subtask.status === 'done' || subtask.status === 2 || subtask.status === 'archived' || subtask.status === 3
|
||||
const completedCount = subtasks.filter(
|
||||
(subtask) =>
|
||||
subtask.status === 'done' ||
|
||||
subtask.status === 2 ||
|
||||
subtask.status === 'archived' ||
|
||||
subtask.status === 3
|
||||
).length;
|
||||
return Math.round((completedCount / subtasks.length) * 100);
|
||||
};
|
||||
|
||||
|
||||
const completionPercentage = calculateCompletionPercentage();
|
||||
|
||||
// Helper function to check if task has subtasks
|
||||
const checkSubtasks = async () => {
|
||||
if (!task.id) return;
|
||||
|
||||
|
||||
try {
|
||||
const subtasksData = await fetchSubtasks(task.id);
|
||||
setHasSubtasks(subtasksData.length > 0);
|
||||
|
|
@ -186,7 +231,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
|
||||
const loadSubtasks = async () => {
|
||||
if (!task.id) return;
|
||||
|
||||
|
||||
setLoadingSubtasks(true);
|
||||
try {
|
||||
const subtasksData = await fetchSubtasks(task.id);
|
||||
|
|
@ -198,7 +243,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
setLoadingSubtasks(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Reload subtasks when showSubtasks changes to true
|
||||
useEffect(() => {
|
||||
if (showSubtasks && subtasks.length === 0) {
|
||||
|
|
@ -208,11 +253,11 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
|
||||
const handleSubtasksToggle = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
if (!showSubtasks && subtasks.length === 0) {
|
||||
await loadSubtasks();
|
||||
}
|
||||
|
||||
|
||||
setShowSubtasks(!showSubtasks);
|
||||
};
|
||||
|
||||
|
|
@ -360,14 +405,18 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
hasSubtasks={hasSubtasks}
|
||||
onSubtasksToggle={handleSubtasksToggle}
|
||||
/>
|
||||
|
||||
|
||||
{/* Progress bar at bottom of parent task */}
|
||||
{subtasks.length > 0 && (
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-0.5 transition-all duration-300 ease-in-out overflow-hidden rounded-b-lg ${
|
||||
showSubtasks ? 'opacity-100 transform translate-y-0' : 'opacity-0 transform translate-y-2'
|
||||
}`}>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 h-0.5 transition-all duration-300 ease-in-out overflow-hidden rounded-b-lg ${
|
||||
showSubtasks
|
||||
? 'opacity-100 transform translate-y-0'
|
||||
: 'opacity-0 transform translate-y-2'
|
||||
}`}
|
||||
>
|
||||
<div className="w-full h-full bg-gray-200 dark:bg-gray-700">
|
||||
<div
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-green-400 via-green-500 to-green-600 transition-all duration-500 ease-out"
|
||||
style={{ width: `${completionPercentage}%` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
const [taskIntelligenceEnabled, setTaskIntelligenceEnabled] =
|
||||
useState(true);
|
||||
const [isTimelineExpanded, setIsTimelineExpanded] = useState(false);
|
||||
const [subtasks, setSubtasks] = useState<Array<{id?: number; name: string; isNew?: boolean}>>([]);
|
||||
const [subtasks, setSubtasks] = useState<
|
||||
Array<{ id?: number; name: string; isNew?: boolean }>
|
||||
>([]);
|
||||
const [subtasksLoaded, setSubtasksLoaded] = useState(false);
|
||||
|
||||
// Collapsible section states
|
||||
|
|
@ -94,11 +96,13 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
|
||||
const scrollToSubtasksSection = () => {
|
||||
setTimeout(() => {
|
||||
const subtasksSection = document.querySelector('[data-section="subtasks"]');
|
||||
const subtasksSection = document.querySelector(
|
||||
'[data-section="subtasks"]'
|
||||
);
|
||||
if (subtasksSection) {
|
||||
subtasksSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end'
|
||||
subtasksSection.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'end',
|
||||
});
|
||||
}
|
||||
}, 300); // Give time for section to expand
|
||||
|
|
@ -204,9 +208,9 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
if (isOpen && autoFocusSubtasks) {
|
||||
// Small delay to ensure modal is fully rendered
|
||||
setTimeout(() => {
|
||||
setExpandedSections(prev => ({
|
||||
setExpandedSections((prev) => ({
|
||||
...prev,
|
||||
subtasks: true
|
||||
subtasks: true,
|
||||
}));
|
||||
scrollToSubtasksSection();
|
||||
}, 100);
|
||||
|
|
@ -316,10 +320,10 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSave({
|
||||
...formData,
|
||||
onSave({
|
||||
...formData,
|
||||
tags: tags.map((tag) => ({ name: tag })),
|
||||
subtasks: subtasks
|
||||
subtasks: subtasks,
|
||||
} as any);
|
||||
const taskLink = (
|
||||
<span>
|
||||
|
|
@ -437,7 +441,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
const loadExistingSubtasks = async () => {
|
||||
try {
|
||||
const existingSubtasks = await fetchSubtasks(task.id!);
|
||||
const subtaskData = existingSubtasks.map(subtask => ({
|
||||
const subtaskData = existingSubtasks.map((subtask) => ({
|
||||
id: subtask.id,
|
||||
name: subtask.name,
|
||||
isNew: false,
|
||||
|
|
@ -450,7 +454,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
setSubtasksLoaded(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadExistingSubtasks();
|
||||
} else if (!isOpen) {
|
||||
// Reset subtasks when modal closes
|
||||
|
|
@ -661,7 +665,10 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
)}
|
||||
|
||||
{expandedSections.subtasks && (
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4" data-section="subtasks">
|
||||
<div
|
||||
className="border-b border-gray-200 dark:border-gray-700 pb-4 mb-4 px-4"
|
||||
data-section="subtasks"
|
||||
>
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||
{t(
|
||||
'forms.task.subtasks',
|
||||
|
|
@ -669,10 +676,16 @@ const TaskModal: React.FC<TaskModalProps> = ({
|
|||
)}
|
||||
</h3>
|
||||
<TaskSubtasksSection
|
||||
parentTaskId={task.id!}
|
||||
parentTaskId={
|
||||
task.id!
|
||||
}
|
||||
subtasks={subtasks}
|
||||
onSubtasksChange={setSubtasks}
|
||||
onSectionMount={scrollToSubtasksSection}
|
||||
onSubtasksChange={
|
||||
setSubtasks
|
||||
}
|
||||
onSectionMount={
|
||||
scrollToSubtasksSection
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,13 @@ const TaskPriorityIcon: React.FC<TaskPriorityIconProps> = ({
|
|||
onToggleCompletion,
|
||||
}) => {
|
||||
const getIconColor = () => {
|
||||
if (status === 'done' || status === 2 || status === 'archived' || status === 3) return 'text-green-500';
|
||||
if (
|
||||
status === 'done' ||
|
||||
status === 2 ||
|
||||
status === 'archived' ||
|
||||
status === 3
|
||||
)
|
||||
return 'text-green-500';
|
||||
|
||||
// Handle both string and numeric priority values
|
||||
let priorityStr = priority;
|
||||
|
|
@ -45,7 +51,12 @@ const TaskPriorityIcon: React.FC<TaskPriorityIconProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
if (status === 'done' || status === 2 || status === 'archived' || status === 3) {
|
||||
if (
|
||||
status === 'done' ||
|
||||
status === 2 ||
|
||||
status === 'archived' ||
|
||||
status === 3
|
||||
) {
|
||||
return (
|
||||
<CheckCircleIcon
|
||||
className={`h-5 w-5 ${colorClass} cursor-pointer hover:scale-110 transition-transform`}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue