Lint and formatting fixes

This commit is contained in:
antanst 2025-07-17 12:46:50 +03:00 committed by Chris
parent 2d4e65bb6e
commit 5123ca95e9
12 changed files with 346 additions and 199 deletions

View file

@ -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');
},
};

View file

@ -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

View file

@ -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');

View file

@ -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
);
});
});

View file

@ -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);
});
});
});

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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);
};

View file

@ -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}%` }}
/>

View file

@ -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>
)}

View file

@ -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`}