diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9621dc0..57f8b8d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -45,7 +45,7 @@ Fixes # ## Checklist -- [ ] This is not an AI slop crap0'/|, I know what I am doing +- [ ] This is not an AI slop crap, I know what I am doing - [ ] Code follows project style guidelines - [ ] Self-reviewed my own code - [ ] Added/updated tests (if applicable) diff --git a/backend/routes/tasks.js b/backend/routes/tasks.js index ad4f285..12e9feb 100644 --- a/backend/routes/tasks.js +++ b/backend/routes/tasks.js @@ -1560,7 +1560,7 @@ router.post('/task', async (req, res) => { ? typeof priority === 'string' ? Task.getPriorityValue(priority) : priority - : Task.PRIORITY.LOW, + : null, due_date: processDueDateForStorage( due_date, getSafeTimezone(req.currentUser.timezone) diff --git a/e2e/tests/priority-defaults.spec.ts b/e2e/tests/priority-defaults.spec.ts new file mode 100644 index 0000000..d08e393 --- /dev/null +++ b/e2e/tests/priority-defaults.spec.ts @@ -0,0 +1,261 @@ +import { test, expect, Page } from '@playwright/test'; +import { + login, + navigateAndWait, + waitForElement, + hoverAndWaitForVisible, + createUniqueEntity, + waitForNetworkIdle +} from '../helpers/testHelpers'; + +// Helper to create a task +async function createTask(page: Page, taskName: string) { + const taskInput = page.locator('[data-testid="new-task-input"]'); + await taskInput.fill(taskName); + await taskInput.press('Enter'); + await waitForNetworkIdle(page); +} + +// Helper to open task edit modal +async function openTaskEditModal(page: Page, taskName: string) { + const taskContainer = page.locator('[data-testid*="task-item"]').filter({ hasText: taskName }); + await expect(taskContainer).toBeVisible({ timeout: 15000 }); + + const editButton = taskContainer.locator('[data-testid*="task-edit"]'); + await hoverAndWaitForVisible(taskContainer, editButton); + + await editButton.click(); + + const taskNameInput = page.locator('[data-testid="task-name-input"]'); + await waitForElement(taskNameInput, { timeout: 15000 }); + + return taskNameInput; +} + +// Helper to create a project +async function createProject(page: Page, projectName: string) { + // Find the "Add Project" button in the sidebar + const addProjectButton = page.locator('button[aria-label="Add Project"]'); + await expect(addProjectButton).toBeVisible(); + + // Click the Add Project button + await addProjectButton.click(); + + // Wait for the Project Modal to appear + await expect(page.locator('input[name="name"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the project name + const nameInput = page.locator('[data-testid="project-name-input"]'); + await nameInput.click(); + await nameInput.clear(); + await nameInput.fill(projectName); + + // Verify the field has the expected value + await expect(nameInput).toHaveValue(projectName, { timeout: 2000 }); + + // Wait for the save button to be enabled + const saveButton = page.locator('[data-testid="project-save-button"]'); + await expect(saveButton).toBeEnabled(); + + // Save the project + await saveButton.click(); + + // Wait for modal to close + await expect(page.locator('[data-testid="project-modal"]')).not.toBeVisible({ timeout: 15000 }); +} + +// Helper to open project edit modal +async function openProjectEditModal(page: Page, projectName: string) { + // Click on the project in the projects list + const projectCard = page.locator('[data-testid*="project-card"]').filter({ hasText: projectName }); + await expect(projectCard).toBeVisible({ timeout: 15000 }); + + // Click the edit button + const editButton = projectCard.locator('[data-testid*="project-edit"]'); + await expect(editButton).toBeVisible(); + await editButton.click(); + + // Wait for modal to open + const nameInput = page.locator('[data-testid="project-name-input"]'); + await waitForElement(nameInput, { timeout: 15000 }); + + return nameInput; +} + +test('task created without priority selection defaults to None', async ({ page, baseURL }) => { + const appUrl = await login(page, baseURL); + + await navigateAndWait(page, appUrl + '/tasks'); + + // Create a task without setting any priority + const taskName = createUniqueEntity('Default Priority Task'); + await createTask(page, taskName); + + // Open the task edit modal + await openTaskEditModal(page, taskName); + + // Wait for modal to be in idle state + await expect(page.locator('[data-testid="task-modal"][data-state="idle"]')).toBeVisible(); + + // Click the priority section icon to expand the priority section + const prioritySectionButton = page.locator('button[title*="Priority"]').filter({ has: page.locator('svg') }); + await prioritySectionButton.click(); + + // Wait for priority section to be expanded + await expect(page.locator('[data-testid="priority-section"][data-state="expanded"]')).toBeVisible(); + + // Check that the priority dropdown shows "None" + const priorityDropdown = page.locator('[data-testid="priority-dropdown"]'); + await expect(priorityDropdown).toBeVisible(); + + // The dropdown should contain "None" text + await expect(priorityDropdown).toContainText(/none/i); + + // Close modal without saving + await page.keyboard.press('Escape'); + await expect(page.locator('[data-testid="task-modal"]')).not.toBeVisible({ timeout: 5000 }); +}); + +test('project created without priority selection defaults to None', async ({ page, baseURL }) => { + const appUrl = await login(page, baseURL); + + await navigateAndWait(page, appUrl + '/projects'); + + // Click the "Add Project" button + const addProjectButton = page.locator('button[aria-label="Add Project"]'); + await expect(addProjectButton).toBeVisible(); + await addProjectButton.click(); + + // Wait for the Project Modal to appear + await expect(page.locator('[data-testid="project-modal"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the project name + const projectName = createUniqueEntity('Default Priority Project'); + const nameInput = page.locator('[data-testid="project-name-input"]'); + await nameInput.fill(projectName); + + // Click the priority section icon to expand the priority section + const prioritySectionButton = page.locator('button[title*="Priority"]').filter({ has: page.locator('svg') }); + await expect(prioritySectionButton).toBeVisible(); + await prioritySectionButton.click(); + + // Wait for priority section to be expanded + await page.waitForTimeout(300); + + // Check that the priority dropdown shows "None" + const priorityDropdown = page.locator('[data-testid="priority-dropdown"]'); + await expect(priorityDropdown).toBeVisible(); + + // The dropdown should contain "None" text + await expect(priorityDropdown).toContainText(/none/i); + + // Close modal without saving + await page.keyboard.press('Escape'); + await expect(page.locator('[data-testid="project-modal"]')).not.toBeVisible({ timeout: 5000 }); +}); + +test('task priority can be set to None after being set to High', async ({ page, baseURL }) => { + const appUrl = await login(page, baseURL); + + await navigateAndWait(page, appUrl + '/tasks'); + + // Create a task + const taskName = createUniqueEntity('Priority Clear Task'); + await createTask(page, taskName); + + // Open the task edit modal + await openTaskEditModal(page, taskName); + + // Wait for modal to be in idle state + await expect(page.locator('[data-testid="task-modal"][data-state="idle"]')).toBeVisible(); + + // Expand priority section + const prioritySectionButton = page.locator('button[title*="Priority"]').filter({ has: page.locator('svg') }); + await prioritySectionButton.click(); + await expect(page.locator('[data-testid="priority-section"][data-state="expanded"]')).toBeVisible(); + + // Set priority to High + const priorityDropdown = page.locator('[data-testid="priority-dropdown"]'); + await priorityDropdown.click(); + await expect(page.locator('[data-testid="priority-dropdown"][data-state="open"]')).toBeVisible(); + + const highPriorityOption = page.locator('[data-testid="priority-option-high"]'); + await expect(highPriorityOption).toBeVisible(); + await highPriorityOption.click(); + + await expect(page.locator('[data-testid="priority-dropdown"][data-state="closed"]')).toBeVisible(); + + // Verify High is selected + await expect(priorityDropdown).toContainText(/high/i); + + // Now clear the priority by selecting None + await priorityDropdown.click(); + await expect(page.locator('[data-testid="priority-dropdown"][data-state="open"]')).toBeVisible(); + + const nonePriorityOption = page.locator('[data-testid="priority-option-none"]'); + await expect(nonePriorityOption).toBeVisible(); + await nonePriorityOption.click(); + + await expect(page.locator('[data-testid="priority-dropdown"][data-state="closed"]')).toBeVisible(); + + // Verify None is now selected + await expect(priorityDropdown).toContainText(/none/i); + + // Close modal without saving + await page.keyboard.press('Escape'); + await expect(page.locator('[data-testid="task-modal"]')).not.toBeVisible({ timeout: 5000 }); +}); + +test('project priority can be set to None after being set to Medium', async ({ page, baseURL }) => { + const appUrl = await login(page, baseURL); + + await navigateAndWait(page, appUrl + '/projects'); + + // Click the "Add Project" button + const addProjectButton = page.locator('button[aria-label="Add Project"]'); + await expect(addProjectButton).toBeVisible(); + await addProjectButton.click(); + + // Wait for the Project Modal to appear + await expect(page.locator('[data-testid="project-modal"]')).toBeVisible({ timeout: 10000 }); + + // Fill in the project name + const projectName = createUniqueEntity('Priority Clear Project'); + const nameInput = page.locator('[data-testid="project-name-input"]'); + await nameInput.fill(projectName); + + // Expand priority section + const prioritySectionButton = page.locator('button[title*="Priority"]').filter({ has: page.locator('svg') }); + await expect(prioritySectionButton).toBeVisible(); + await prioritySectionButton.click(); + await page.waitForTimeout(300); + + // Set priority to Medium + const priorityDropdown = page.locator('[data-testid="priority-dropdown"]'); + await priorityDropdown.click(); + await page.waitForTimeout(200); + + const mediumPriorityOption = page.locator('[data-testid="priority-option-medium"]'); + await expect(mediumPriorityOption).toBeVisible(); + await mediumPriorityOption.click(); + await page.waitForTimeout(200); + + // Verify Medium is selected + await expect(priorityDropdown).toContainText(/medium/i); + + // Now clear the priority by selecting None + await priorityDropdown.click(); + await page.waitForTimeout(200); + + const nonePriorityOption = page.locator('[data-testid="priority-option-none"]'); + await expect(nonePriorityOption).toBeVisible(); + await nonePriorityOption.click(); + await page.waitForTimeout(200); + + // Verify None is now selected + await expect(priorityDropdown).toContainText(/none/i); + + // Close modal without saving + await page.keyboard.press('Escape'); + await expect(page.locator('[data-testid="project-modal"]')).not.toBeVisible({ timeout: 5000 }); +}); diff --git a/e2e/tests/task-priority-due-date.spec.ts b/e2e/tests/task-priority-due-date.spec.ts index 1028a15..07048c5 100644 --- a/e2e/tests/task-priority-due-date.spec.ts +++ b/e2e/tests/task-priority-due-date.spec.ts @@ -55,7 +55,7 @@ test('user can set task priority to high', async ({ page, baseURL }) => { await expect(page.locator('[data-testid="priority-section"][data-state="expanded"]')).toBeVisible(); // Wait for priority dropdown to be ready, then click it - const priorityDropdown = page.locator('.inline-flex.justify-between').filter({ hasText: /low|medium|high/i }).first(); + const priorityDropdown = page.locator('[data-testid="priority-dropdown"]'); await expect(priorityDropdown).toBeVisible(); await priorityDropdown.click(); @@ -63,7 +63,7 @@ test('user can set task priority to high', async ({ page, baseURL }) => { await expect(page.locator('[data-testid="priority-dropdown"][data-state="open"]')).toBeVisible(); // Select "High" priority from the portal dropdown - const highPriorityOption = page.locator('button').filter({ hasText: /high/i }).first(); + const highPriorityOption = page.locator('[data-testid="priority-option-high"]'); await expect(highPriorityOption).toBeVisible(); await highPriorityOption.click(); @@ -103,12 +103,12 @@ test('user can set task priority to medium and low', async ({ page, baseURL }) = await expect(page.locator('[data-testid="priority-section"][data-state="expanded"]')).toBeVisible(); // Set to medium priority - const priorityDropdown = page.locator('.inline-flex.justify-between').filter({ hasText: /low|medium|high/i }).first(); + const priorityDropdown = page.locator('[data-testid="priority-dropdown"]'); await expect(priorityDropdown).toBeVisible(); await priorityDropdown.click(); await expect(page.locator('[data-testid="priority-dropdown"][data-state="open"]')).toBeVisible(); - const mediumPriorityOption = page.locator('button').filter({ hasText: /medium/i }).first(); + const mediumPriorityOption = page.locator('[data-testid="priority-option-medium"]'); await expect(mediumPriorityOption).toBeVisible(); await mediumPriorityOption.click(); await expect(page.locator('[data-testid="priority-dropdown"][data-state="closed"]')).toBeVisible(); @@ -127,7 +127,7 @@ test('user can set task priority to medium and low', async ({ page, baseURL }) = await expect(page.locator('[data-testid="task-modal"][data-state="idle"]')).toBeVisible(); // Check if priority section is already expanded (it should be for non-default priority) - const priorityDropdown2 = page.locator('.inline-flex.justify-between').filter({ hasText: /low|medium|high/i }).first(); + const priorityDropdown2 = page.locator('[data-testid="priority-dropdown"]'); const isAlreadyExpanded = await priorityDropdown2.isVisible().catch(() => false); if (!isAlreadyExpanded) { @@ -140,7 +140,7 @@ test('user can set task priority to medium and low', async ({ page, baseURL }) = await priorityDropdown2.click(); await expect(page.locator('[data-testid="priority-dropdown"][data-state="open"]')).toBeVisible(); - const lowPriorityOption = page.locator('button').filter({ hasText: /low/i }).first(); + const lowPriorityOption = page.locator('[data-testid="priority-option-low"]'); await expect(lowPriorityOption).toBeVisible(); await lowPriorityOption.click(); await expect(page.locator('[data-testid="priority-dropdown"][data-state="closed"]')).toBeVisible(); diff --git a/frontend/components/Project/ProjectModal.tsx b/frontend/components/Project/ProjectModal.tsx index ca903db..3075270 100644 --- a/frontend/components/Project/ProjectModal.tsx +++ b/frontend/components/Project/ProjectModal.tsx @@ -47,7 +47,7 @@ const ProjectModal: React.FC = ({ area_id: null, state: 'idea', tags: [], - priority: 'low', + priority: null, due_date_at: null, image_url: '', } @@ -139,7 +139,7 @@ const ProjectModal: React.FC = ({ area_id: null, state: 'idea', tags: [], - priority: 'low', + priority: null, due_date_at: null, image_url: '', }); @@ -688,8 +688,8 @@ const ProjectModal: React.FC = ({ = ({ )} > - {formData.priority !== 'medium' && ( + {formData.priority != null && ( )} diff --git a/frontend/components/Shared/PriorityDropdown.tsx b/frontend/components/Shared/PriorityDropdown.tsx index 7f9ac85..edca558 100644 --- a/frontend/components/Shared/PriorityDropdown.tsx +++ b/frontend/components/Shared/PriorityDropdown.tsx @@ -5,6 +5,7 @@ import { ArrowDownIcon, ArrowUpIcon, FireIcon, + XMarkIcon, } from '@heroicons/react/24/outline'; import { PriorityType } from '../../entities/Task'; import { useTranslation } from 'react-i18next'; @@ -21,6 +22,13 @@ const PriorityDropdown: React.FC = ({ const { t } = useTranslation(); const priorities = [ + { + value: null, + label: t('priority.none', 'None'), + icon: ( + + ), + }, { value: 'low', label: t('priority.low', 'Low'), @@ -102,13 +110,14 @@ const PriorityDropdown: React.FC = ({ }, [isOpen]); // Convert numeric priority to string if needed + // Don't default to any value - allow null/undefined const normalizedValue = typeof value === 'number' - ? ['low', 'medium', 'high'][value] || 'medium' + ? (['low', 'medium', 'high'][value] as PriorityType) : value; const selectedPriority = priorities.find( - (p) => p.value === normalizedValue + (p) => p.value === (normalizedValue || null) ); return ( @@ -152,6 +161,7 @@ const PriorityDropdown: React.FC = ({ handleSelect(priority.value as PriorityType) } className="flex items-center justify-between px-4 py-2 text-sm text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-600 w-full first:rounded-t-md last:rounded-b-md" + data-testid={`priority-option-${priority.value || 'none'}`} > {priority.icon}{' '} diff --git a/frontend/components/Task/TaskModal.tsx b/frontend/components/Task/TaskModal.tsx index 79cb460..80fdc2a 100644 --- a/frontend/components/Task/TaskModal.tsx +++ b/frontend/components/Task/TaskModal.tsx @@ -270,10 +270,14 @@ const TaskModal: React.FC = ({ priority: PriorityType | number | undefined ): PriorityType => { if (typeof priority === 'number') { - const priorityNames: PriorityType[] = ['low', 'medium', 'high']; - return priorityNames[priority] || 'low'; + const priorityNames: ('low' | 'medium' | 'high')[] = [ + 'low', + 'medium', + 'high', + ]; + return priorityNames[priority] || null; } - return priority || 'low'; + return priority ?? null; }; const handleChange = ( @@ -820,9 +824,7 @@ const TaskModal: React.FC = ({ )} > - {getPriorityString( - formData.priority - ) !== 'medium' && ( + {formData.priority != null && ( )} diff --git a/frontend/components/Task/TaskPriorityIcon.tsx b/frontend/components/Task/TaskPriorityIcon.tsx index 3b20adb..b6e6d5d 100644 --- a/frontend/components/Task/TaskPriorityIcon.tsx +++ b/frontend/components/Task/TaskPriorityIcon.tsx @@ -19,7 +19,7 @@ const TaskPriorityIcon: React.FC = ({ let priorityStr = priority; if (typeof priority === 'number') { const priorityNames = ['low', 'medium', 'high']; - priorityStr = priorityNames[priority] || 'low'; + priorityStr = priorityNames[priority]; } switch (priorityStr) { @@ -31,8 +31,13 @@ const TaskPriorityIcon: React.FC = ({ return 'Medium priority'; case 'low': case 0: - default: return 'Low priority'; + case null: + case undefined: + case '': + return ''; // No priority set + default: + return ''; // Default to no priority text } }; @@ -49,7 +54,7 @@ const TaskPriorityIcon: React.FC = ({ let priorityStr = priority; if (typeof priority === 'number') { const priorityNames = ['low', 'medium', 'high']; - priorityStr = priorityNames[priority] || 'low'; + priorityStr = priorityNames[priority]; } switch (priorityStr) { @@ -61,12 +66,17 @@ const TaskPriorityIcon: React.FC = ({ return 'text-yellow-500'; case 'low': case 0: - default: return 'text-gray-300'; + case null: + case undefined: + case '': + default: + return 'text-gray-300'; // No priority - use gray } }; const colorClass = getIconColor(); + const priorityText = getPriorityText(); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); // Prevent triggering TaskHeader onClick @@ -89,7 +99,7 @@ const TaskPriorityIcon: React.FC = ({ marginRight: '-2px', }} onClick={handleClick} - title={getPriorityText()} + {...(priorityText && { title: priorityText })} role="checkbox" aria-checked="true" data-testid={`task-completion-checkbox${testIdSuffix}`} @@ -100,7 +110,7 @@ const TaskPriorityIcon: React.FC = ({