Sorting fixes (#174)
* Update version * Order Inbox items by creation timestamp, desc * Fix input keyboard tab and enter handling * Fix lint issues * Move in progress items to the top of today list * Make tags and projects clickable * fixup! Make tags and projects clickable * fixup! fixup! Make tags and projects clickable
This commit is contained in:
parent
b37457b574
commit
10d96397c3
12 changed files with 339 additions and 29 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
files: ['**/*.js'],
|
files: ['**/*.js'],
|
||||||
|
ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2022,
|
ecmaVersion: 2022,
|
||||||
sourceType: 'commonjs',
|
sourceType: 'commonjs',
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,11 @@ async function filterTasksByParams(params, userId) {
|
||||||
|
|
||||||
let orderClause = [['created_at', 'ASC']];
|
let orderClause = [['created_at', 'ASC']];
|
||||||
|
|
||||||
|
// Special ordering for inbox - newest items first
|
||||||
|
if (params.type === 'inbox') {
|
||||||
|
orderClause = [['created_at', 'DESC']];
|
||||||
|
}
|
||||||
|
|
||||||
// Apply ordering
|
// Apply ordering
|
||||||
if (params.order_by) {
|
if (params.order_by) {
|
||||||
const [orderColumn, orderDirection = 'asc'] =
|
const [orderColumn, orderDirection = 'asc'] =
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,50 @@ describe('Inbox Routes', () => {
|
||||||
expect(response.body[0].status).toBe('added');
|
expect(response.body[0].status).toBe('added');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return inbox items ordered by created_at DESC (newest first)', async () => {
|
||||||
|
// Create additional items with slight delay to ensure different timestamps
|
||||||
|
const item1 = await InboxItem.create({
|
||||||
|
content: 'First item (oldest)',
|
||||||
|
status: 'added',
|
||||||
|
source: 'test',
|
||||||
|
user_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Small delay to ensure different timestamps
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
const item2 = await InboxItem.create({
|
||||||
|
content: 'Second item',
|
||||||
|
status: 'added',
|
||||||
|
source: 'test',
|
||||||
|
user_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
|
||||||
|
const item3 = await InboxItem.create({
|
||||||
|
content: 'Third item (newest)',
|
||||||
|
status: 'added',
|
||||||
|
source: 'test',
|
||||||
|
user_id: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await agent.get('/api/inbox');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.length).toBe(4); // Including the item from beforeEach
|
||||||
|
|
||||||
|
// Check that items are ordered by newest first
|
||||||
|
expect(response.body[0].id).toBe(item3.id);
|
||||||
|
expect(response.body[1].id).toBe(item2.id);
|
||||||
|
expect(response.body[2].id).toBe(item1.id);
|
||||||
|
|
||||||
|
// Verify the content matches expected order
|
||||||
|
expect(response.body[0].content).toBe('Third item (newest)');
|
||||||
|
expect(response.body[1].content).toBe('Second item');
|
||||||
|
expect(response.body[2].content).toBe('First item (oldest)');
|
||||||
|
});
|
||||||
|
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const response = await request(app).get('/api/inbox');
|
const response = await request(app).get('/api/inbox');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import { InboxItem } from '../../entities/InboxItem';
|
import { InboxItem } from '../../entities/InboxItem';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
|
|
@ -400,7 +401,50 @@ const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
|
||||||
{projectRefs.length > 0 && (
|
{projectRefs.length > 0 && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FolderIcon className="h-3 w-3 mr-1" />
|
<FolderIcon className="h-3 w-3 mr-1" />
|
||||||
<span>{projectRefs.join(', ')}</span>
|
<span>
|
||||||
|
{projectRefs.map(
|
||||||
|
(projectRef, index) => {
|
||||||
|
// Find matching project
|
||||||
|
const matchingProject =
|
||||||
|
projects.find(
|
||||||
|
(project) =>
|
||||||
|
project.name.toLowerCase() ===
|
||||||
|
projectRef.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingProject) {
|
||||||
|
return (
|
||||||
|
<React.Fragment
|
||||||
|
key={projectRef}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`/project/${matchingProject.id}`}
|
||||||
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{projectRef}
|
||||||
|
</Link>
|
||||||
|
{index <
|
||||||
|
projectRefs.length -
|
||||||
|
1 && ', '}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<React.Fragment
|
||||||
|
key={projectRef}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{projectRef}
|
||||||
|
</span>
|
||||||
|
{index <
|
||||||
|
projectRefs.length -
|
||||||
|
1 && ', '}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -413,7 +457,23 @@ const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
|
||||||
{hashtags.length > 0 && (
|
{hashtags.length > 0 && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<TagIcon className="h-3 w-3 mr-1" />
|
<TagIcon className="h-3 w-3 mr-1" />
|
||||||
<span>{hashtags.join(', ')}</span>
|
<span>
|
||||||
|
{hashtags.map((hashtag, index) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={hashtag}>
|
||||||
|
<Link
|
||||||
|
to={`/tag/${encodeURIComponent(hashtag)}`}
|
||||||
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{hashtag}
|
||||||
|
</Link>
|
||||||
|
{index <
|
||||||
|
hashtags.length - 1 &&
|
||||||
|
', '}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
});
|
});
|
||||||
|
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
||||||
// const [urlPreview, setUrlPreview] = useState<UrlTitleResult | null>(null);
|
// const [urlPreview, setUrlPreview] = useState<UrlTitleResult | null>(null);
|
||||||
|
|
||||||
// Real-time text analysis state
|
// Real-time text analysis state
|
||||||
|
|
@ -542,6 +543,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
// Hide project suggestions when showing tag suggestions
|
// Hide project suggestions when showing tag suggestions
|
||||||
setShowProjectSuggestions(false);
|
setShowProjectSuggestions(false);
|
||||||
setFilteredProjects([]);
|
setFilteredProjects([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
|
||||||
// Filter tags based on current query
|
// Filter tags based on current query
|
||||||
const filtered = tags
|
const filtered = tags
|
||||||
|
|
@ -561,6 +563,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
|
|
||||||
setFilteredTags(filtered);
|
setFilteredTags(filtered);
|
||||||
setShowTagSuggestions(true);
|
setShowTagSuggestions(true);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
} else if (
|
} else if (
|
||||||
(newText.charAt(newCursorPosition - 1) === '+' || projectQuery) &&
|
(newText.charAt(newCursorPosition - 1) === '+' || projectQuery) &&
|
||||||
projectQuery !== ''
|
projectQuery !== ''
|
||||||
|
|
@ -568,6 +571,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
// Hide tag suggestions when showing project suggestions
|
// Hide tag suggestions when showing project suggestions
|
||||||
setShowTagSuggestions(false);
|
setShowTagSuggestions(false);
|
||||||
setFilteredTags([]);
|
setFilteredTags([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
|
||||||
// Filter projects based on current query
|
// Filter projects based on current query
|
||||||
const filtered = projects
|
const filtered = projects
|
||||||
|
|
@ -587,11 +591,13 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
|
|
||||||
setFilteredProjects(filtered);
|
setFilteredProjects(filtered);
|
||||||
setShowProjectSuggestions(true);
|
setShowProjectSuggestions(true);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
} else {
|
} else {
|
||||||
setShowTagSuggestions(false);
|
setShowTagSuggestions(false);
|
||||||
setFilteredTags([]);
|
setFilteredTags([]);
|
||||||
setShowProjectSuggestions(false);
|
setShowProjectSuggestions(false);
|
||||||
setFilteredProjects([]);
|
setFilteredProjects([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -783,6 +789,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
setInputText(newText);
|
setInputText(newText);
|
||||||
setShowTagSuggestions(false);
|
setShowTagSuggestions(false);
|
||||||
setFilteredTags([]);
|
setFilteredTags([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
|
||||||
// Focus back on input and set cursor position
|
// Focus back on input and set cursor position
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -850,6 +857,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
setInputText(newText);
|
setInputText(newText);
|
||||||
setShowProjectSuggestions(false);
|
setShowProjectSuggestions(false);
|
||||||
setFilteredProjects([]);
|
setFilteredProjects([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
|
||||||
// Focus back on input and set cursor position
|
// Focus back on input and set cursor position
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -1231,9 +1239,11 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
if (showTagSuggestions) {
|
if (showTagSuggestions) {
|
||||||
setShowTagSuggestions(false);
|
setShowTagSuggestions(false);
|
||||||
setFilteredTags([]);
|
setFilteredTags([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
} else if (showProjectSuggestions) {
|
} else if (showProjectSuggestions) {
|
||||||
setShowProjectSuggestions(false);
|
setShowProjectSuggestions(false);
|
||||||
setFilteredProjects([]);
|
setFilteredProjects([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
} else {
|
} else {
|
||||||
handleClose();
|
handleClose();
|
||||||
}
|
}
|
||||||
|
|
@ -1253,9 +1263,11 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
if (showTagSuggestions) {
|
if (showTagSuggestions) {
|
||||||
setShowTagSuggestions(false);
|
setShowTagSuggestions(false);
|
||||||
setFilteredTags([]);
|
setFilteredTags([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
} else if (showProjectSuggestions) {
|
} else if (showProjectSuggestions) {
|
||||||
setShowProjectSuggestions(false);
|
setShowProjectSuggestions(false);
|
||||||
setFilteredProjects([]);
|
setFilteredProjects([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
} else {
|
} else {
|
||||||
handleClose();
|
handleClose();
|
||||||
}
|
}
|
||||||
|
|
@ -1357,12 +1369,130 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
className="w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white focus:outline-none shadow-sm py-2"
|
className="w-full text-xl font-semibold dark:bg-gray-800 text-black dark:text-white focus:outline-none shadow-sm py-2"
|
||||||
placeholder={t('inbox.captureThought')}
|
placeholder={t('inbox.captureThought')}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
// Handle dropdown navigation
|
||||||
|
if (
|
||||||
|
showTagSuggestions &&
|
||||||
|
filteredTags.length > 0
|
||||||
|
) {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedSuggestionIndex(
|
||||||
|
(prev) =>
|
||||||
|
prev <
|
||||||
|
filteredTags.length - 1
|
||||||
|
? prev + 1
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedSuggestionIndex(
|
||||||
|
(prev) =>
|
||||||
|
prev > 0
|
||||||
|
? prev - 1
|
||||||
|
: filteredTags.length -
|
||||||
|
1
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const selectedTag =
|
||||||
|
selectedSuggestionIndex >= 0
|
||||||
|
? filteredTags[
|
||||||
|
selectedSuggestionIndex
|
||||||
|
]
|
||||||
|
: filteredTags[0];
|
||||||
|
handleTagSelect(
|
||||||
|
selectedTag.name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
e.key === 'Enter' &&
|
||||||
|
selectedSuggestionIndex >= 0
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleTagSelect(
|
||||||
|
filteredTags[
|
||||||
|
selectedSuggestionIndex
|
||||||
|
].name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowTagSuggestions(false);
|
||||||
|
setFilteredTags([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle project dropdown navigation
|
||||||
|
if (
|
||||||
|
showProjectSuggestions &&
|
||||||
|
filteredProjects.length > 0
|
||||||
|
) {
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedSuggestionIndex(
|
||||||
|
(prev) =>
|
||||||
|
prev <
|
||||||
|
filteredProjects.length -
|
||||||
|
1
|
||||||
|
? prev + 1
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedSuggestionIndex(
|
||||||
|
(prev) =>
|
||||||
|
prev > 0
|
||||||
|
? prev - 1
|
||||||
|
: filteredProjects.length -
|
||||||
|
1
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
e.preventDefault();
|
||||||
|
const selectedProject =
|
||||||
|
selectedSuggestionIndex >= 0
|
||||||
|
? filteredProjects[
|
||||||
|
selectedSuggestionIndex
|
||||||
|
]
|
||||||
|
: filteredProjects[0];
|
||||||
|
handleProjectSelect(
|
||||||
|
selectedProject.name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (
|
||||||
|
e.key === 'Enter' &&
|
||||||
|
selectedSuggestionIndex >= 0
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleProjectSelect(
|
||||||
|
filteredProjects[
|
||||||
|
selectedSuggestionIndex
|
||||||
|
].name
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowProjectSuggestions(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
setFilteredProjects([]);
|
||||||
|
setSelectedSuggestionIndex(-1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
if (
|
if (
|
||||||
e.key === 'Enter' &&
|
e.key === 'Enter' &&
|
||||||
!e.shiftKey &&
|
!e.shiftKey &&
|
||||||
!isSaving
|
!isSaving
|
||||||
) {
|
) {
|
||||||
// If suggestions are showing and there are filtered options, let the user navigate
|
// If suggestions are showing and there are filtered options, don't submit
|
||||||
if (
|
if (
|
||||||
(showTagSuggestions &&
|
(showTagSuggestions &&
|
||||||
filteredTags.length > 0) ||
|
filteredTags.length > 0) ||
|
||||||
|
|
@ -1372,7 +1502,6 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
// Don't submit, let the user select from suggestions
|
// Don't submit, let the user select from suggestions
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, submit the form
|
// Otherwise, submit the form
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
|
|
@ -1549,7 +1678,12 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
tag.name
|
tag.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 text-sm text-gray-900 dark:text-gray-100 first:rounded-t-md last:rounded-b-md"
|
className={`w-full text-left px-3 py-2 text-sm text-gray-900 dark:text-gray-100 first:rounded-t-md last:rounded-b-md ${
|
||||||
|
selectedSuggestionIndex ===
|
||||||
|
index
|
||||||
|
? 'bg-blue-100 dark:bg-blue-800'
|
||||||
|
: 'hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
#{tag.name}
|
#{tag.name}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1580,7 +1714,12 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
||||||
project.name
|
project.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="w-full text-left px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 text-sm text-gray-900 dark:text-gray-100 first:rounded-t-md last:rounded-b-md"
|
className={`w-full text-left px-3 py-2 text-sm text-gray-900 dark:text-gray-100 first:rounded-t-md last:rounded-b-md ${
|
||||||
|
selectedSuggestionIndex ===
|
||||||
|
index
|
||||||
|
? 'bg-blue-100 dark:bg-blue-800'
|
||||||
|
: 'hover:bg-gray-100 dark:hover:bg-gray-600'
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
+{project.name}
|
+{project.name}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const ToastComponent: React.FC<{
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`px-4 py-3 rounded-lg shadow-md text-white transition-all duration-300 ${
|
className={`px-4 py-3 rounded-lg shadow-md text-white transition-all duration-300 ${
|
||||||
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
type === 'success' ? 'bg-green-600' : 'bg-red-500'
|
||||||
}`}
|
}`}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
|
|
@ -140,7 +141,12 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||||
{project && !hideProjectName && (
|
{project && !hideProjectName && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FolderIcon className="h-3 w-3 mr-1" />
|
<FolderIcon className="h-3 w-3 mr-1" />
|
||||||
<span>{project.name}</span>
|
<Link
|
||||||
|
to={`/project/${project.id}`}
|
||||||
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{project.name}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{project &&
|
{project &&
|
||||||
|
|
@ -153,9 +159,19 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<TagIcon className="h-3 w-3 mr-1" />
|
<TagIcon className="h-3 w-3 mr-1" />
|
||||||
<span>
|
<span>
|
||||||
{task.tags
|
{task.tags.map((tag, index) => (
|
||||||
.map((tag) => tag.name)
|
<React.Fragment key={tag.name}>
|
||||||
.join(', ')}
|
<Link
|
||||||
|
to={`/tag/${encodeURIComponent(tag.name)}`}
|
||||||
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</Link>
|
||||||
|
{index <
|
||||||
|
task.tags!.length - 1 &&
|
||||||
|
', '}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -289,16 +305,31 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||||
{project && !hideProjectName && (
|
{project && !hideProjectName && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FolderIcon className="h-3 w-3 mr-1" />
|
<FolderIcon className="h-3 w-3 mr-1" />
|
||||||
<span>{project.name}</span>
|
<Link
|
||||||
|
to={`/project/${project.id}`}
|
||||||
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{project.name}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{task.tags && task.tags.length > 0 && (
|
{task.tags && task.tags.length > 0 && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<TagIcon className="h-3 w-3 mr-1" />
|
<TagIcon className="h-3 w-3 mr-1" />
|
||||||
<span>
|
<span>
|
||||||
{task.tags
|
{task.tags.map((tag, index) => (
|
||||||
.map((tag) => tag.name)
|
<React.Fragment key={tag.name}>
|
||||||
.join(', ')}
|
<Link
|
||||||
|
to={`/tag/${encodeURIComponent(tag.name)}`}
|
||||||
|
className="text-gray-500 dark:text-gray-400 hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{tag.name}
|
||||||
|
</Link>
|
||||||
|
{index <
|
||||||
|
task.tags!.length - 1 &&
|
||||||
|
', '}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 mt-1 ${
|
className={`rounded-lg shadow-sm bg-white dark:bg-gray-900 mt-1 transition-all duration-200 ease-in-out ${
|
||||||
isInProgress
|
isInProgress
|
||||||
? 'border-2 border-green-400/60 dark:border-green-500/60'
|
? 'border-2 border-green-400/60 dark:border-green-500/60'
|
||||||
: 'border-2 border-gray-50 dark:border-gray-800'
|
: 'border-2 border-gray-50 dark:border-gray-800'
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,14 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||||
onToggleToday,
|
onToggleToday,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="task-list-container">
|
||||||
{tasks.length > 0 ? (
|
{tasks.length > 0 ? (
|
||||||
tasks.map((task) => (
|
tasks.map((task) => (
|
||||||
<TaskItem
|
<div
|
||||||
key={task.id}
|
key={task.id}
|
||||||
|
className="task-item-wrapper transition-all duration-200 ease-in-out"
|
||||||
|
>
|
||||||
|
<TaskItem
|
||||||
task={task}
|
task={task}
|
||||||
onTaskUpdate={onTaskUpdate}
|
onTaskUpdate={onTaskUpdate}
|
||||||
onTaskDelete={onTaskDelete}
|
onTaskDelete={onTaskDelete}
|
||||||
|
|
@ -34,6 +37,7 @@ const TaskList: React.FC<TaskListProps> = ({
|
||||||
hideProjectName={hideProjectName}
|
hideProjectName={hideProjectName}
|
||||||
onToggleToday={onToggleToday}
|
onToggleToday={onToggleToday}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-gray-500 dark:text-gray-400 text-center mt-4">
|
<p className="text-gray-500 dark:text-gray-400 text-center mt-4">
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,33 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
|
||||||
// Handle undefined or null todayPlanTasks
|
// Handle undefined or null todayPlanTasks
|
||||||
const safeTodayPlanTasks = todayPlanTasks || [];
|
const safeTodayPlanTasks = todayPlanTasks || [];
|
||||||
|
|
||||||
if (safeTodayPlanTasks.length === 0) {
|
// Sort tasks to move in-progress tasks to the top
|
||||||
|
const sortedTasks = React.useMemo(() => {
|
||||||
|
if (safeTodayPlanTasks.length === 0) return [];
|
||||||
|
|
||||||
|
return [...safeTodayPlanTasks].sort((a, b) => {
|
||||||
|
const aInProgress = a.status === 'in_progress' || a.status === 1;
|
||||||
|
const bInProgress = b.status === 'in_progress' || b.status === 1;
|
||||||
|
|
||||||
|
// If both are in progress, sort by updated_at (recently updated to bottom)
|
||||||
|
if (aInProgress && bInProgress) {
|
||||||
|
// Recently updated tasks should be at the bottom of in-progress group
|
||||||
|
const aUpdated = new Date(a.updated_at || a.created_at || 0);
|
||||||
|
const bUpdated = new Date(b.updated_at || b.created_at || 0);
|
||||||
|
return aUpdated.getTime() - bUpdated.getTime(); // Older tasks first, newer to bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both are not in progress, maintain original order
|
||||||
|
if (!aInProgress && !bInProgress) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put in-progress tasks first
|
||||||
|
return aInProgress ? -1 : 1;
|
||||||
|
});
|
||||||
|
}, [safeTodayPlanTasks]);
|
||||||
|
|
||||||
|
if (sortedTasks.length === 0) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-center items-center mt-4">
|
<div className="flex justify-center items-center mt-4">
|
||||||
|
|
@ -52,7 +78,7 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TaskList
|
<TaskList
|
||||||
tasks={safeTodayPlanTasks}
|
tasks={sortedTasks}
|
||||||
onTaskUpdate={onTaskUpdate}
|
onTaskUpdate={onTaskUpdate}
|
||||||
onTaskDelete={onTaskDelete}
|
onTaskDelete={onTaskDelete}
|
||||||
projects={projects}
|
projects={projects}
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ export const useStore = create<StoreState>((set) => ({
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
inboxStore: {
|
inboxStore: {
|
||||||
...state.inboxStore,
|
...state.inboxStore,
|
||||||
inboxItems: [...state.inboxStore.inboxItems, inboxItem],
|
inboxItems: [inboxItem, ...state.inboxStore.inboxItems],
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
updateInboxItem: (inboxItem) =>
|
updateInboxItem: (inboxItem) =>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "tududi",
|
"name": "tududi",
|
||||||
"version": "v0.72.1",
|
"version": "v0.72.2",
|
||||||
"description": "Self-hosted task management with hierarchical organization (Areas > Projects > Tasks), multi-language support, and Telegram integration. Built with React/TypeScript frontend and functional programming Express.js backend.",
|
"description": "Self-hosted task management with hierarchical organization (Areas > Projects > Tasks), multi-language support, and Telegram integration. Built with React/TypeScript frontend and functional programming Express.js backend.",
|
||||||
"main": "backend/app.js",
|
"main": "backend/app.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue