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 = [
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'commonjs',
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ async function filterTasksByParams(params, userId) {
|
|||
|
||||
let orderClause = [['created_at', 'ASC']];
|
||||
|
||||
// Special ordering for inbox - newest items first
|
||||
if (params.type === 'inbox') {
|
||||
orderClause = [['created_at', 'DESC']];
|
||||
}
|
||||
|
||||
// Apply ordering
|
||||
if (params.order_by) {
|
||||
const [orderColumn, orderDirection = 'asc'] =
|
||||
|
|
|
|||
|
|
@ -95,6 +95,50 @@ describe('Inbox Routes', () => {
|
|||
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 () => {
|
||||
const response = await request(app).get('/api/inbox');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { InboxItem } from '../../entities/InboxItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
|
|
@ -400,7 +401,50 @@ const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
|
|||
{projectRefs.length > 0 && (
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
|
@ -413,7 +457,23 @@ const InboxItemDetail: React.FC<InboxItemDetailProps> = ({
|
|||
{hashtags.length > 0 && (
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
left: 0,
|
||||
top: 0,
|
||||
});
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
||||
// const [urlPreview, setUrlPreview] = useState<UrlTitleResult | null>(null);
|
||||
|
||||
// Real-time text analysis state
|
||||
|
|
@ -542,6 +543,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
// Hide project suggestions when showing tag suggestions
|
||||
setShowProjectSuggestions(false);
|
||||
setFilteredProjects([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
|
||||
// Filter tags based on current query
|
||||
const filtered = tags
|
||||
|
|
@ -561,6 +563,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
|
||||
setFilteredTags(filtered);
|
||||
setShowTagSuggestions(true);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else if (
|
||||
(newText.charAt(newCursorPosition - 1) === '+' || projectQuery) &&
|
||||
projectQuery !== ''
|
||||
|
|
@ -568,6 +571,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
// Hide tag suggestions when showing project suggestions
|
||||
setShowTagSuggestions(false);
|
||||
setFilteredTags([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
|
||||
// Filter projects based on current query
|
||||
const filtered = projects
|
||||
|
|
@ -587,11 +591,13 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
|
||||
setFilteredProjects(filtered);
|
||||
setShowProjectSuggestions(true);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else {
|
||||
setShowTagSuggestions(false);
|
||||
setFilteredTags([]);
|
||||
setShowProjectSuggestions(false);
|
||||
setFilteredProjects([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -783,6 +789,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
setInputText(newText);
|
||||
setShowTagSuggestions(false);
|
||||
setFilteredTags([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
|
||||
// Focus back on input and set cursor position
|
||||
setTimeout(() => {
|
||||
|
|
@ -850,6 +857,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
setInputText(newText);
|
||||
setShowProjectSuggestions(false);
|
||||
setFilteredProjects([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
|
||||
// Focus back on input and set cursor position
|
||||
setTimeout(() => {
|
||||
|
|
@ -1231,9 +1239,11 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
if (showTagSuggestions) {
|
||||
setShowTagSuggestions(false);
|
||||
setFilteredTags([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else if (showProjectSuggestions) {
|
||||
setShowProjectSuggestions(false);
|
||||
setFilteredProjects([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
|
|
@ -1253,9 +1263,11 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
if (showTagSuggestions) {
|
||||
setShowTagSuggestions(false);
|
||||
setFilteredTags([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else if (showProjectSuggestions) {
|
||||
setShowProjectSuggestions(false);
|
||||
setFilteredProjects([]);
|
||||
setSelectedSuggestionIndex(-1);
|
||||
} else {
|
||||
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"
|
||||
placeholder={t('inbox.captureThought')}
|
||||
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 (
|
||||
e.key === 'Enter' &&
|
||||
!e.shiftKey &&
|
||||
!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 (
|
||||
(showTagSuggestions &&
|
||||
filteredTags.length > 0) ||
|
||||
|
|
@ -1372,7 +1502,6 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
// Don't submit, let the user select from suggestions
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, submit the form
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
|
|
@ -1549,7 +1678,12 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
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}
|
||||
</button>
|
||||
|
|
@ -1580,7 +1714,12 @@ const InboxModal: React.FC<InboxModalProps> = ({
|
|||
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}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ const ToastComponent: React.FC<{
|
|||
return (
|
||||
<div
|
||||
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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
CalendarIcon,
|
||||
|
|
@ -140,7 +141,12 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
{project && !hideProjectName && (
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
)}
|
||||
{project &&
|
||||
|
|
@ -153,9 +159,19 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
<div className="flex items-center">
|
||||
<TagIcon className="h-3 w-3 mr-1" />
|
||||
<span>
|
||||
{task.tags
|
||||
.map((tag) => tag.name)
|
||||
.join(', ')}
|
||||
{task.tags.map((tag, index) => (
|
||||
<React.Fragment key={tag.name}>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -289,16 +305,31 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
|||
{project && !hideProjectName && (
|
||||
<div className="flex items-center">
|
||||
<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>
|
||||
)}
|
||||
{task.tags && task.tags.length > 0 && (
|
||||
<div className="flex items-center">
|
||||
<TagIcon className="h-3 w-3 mr-1" />
|
||||
<span>
|
||||
{task.tags
|
||||
.map((tag) => tag.name)
|
||||
.join(', ')}
|
||||
{task.tags.map((tag, index) => (
|
||||
<React.Fragment key={tag.name}>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
|
|||
|
||||
return (
|
||||
<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
|
||||
? 'border-2 border-green-400/60 dark:border-green-500/60'
|
||||
: 'border-2 border-gray-50 dark:border-gray-800'
|
||||
|
|
|
|||
|
|
@ -22,18 +22,22 @@ const TaskList: React.FC<TaskListProps> = ({
|
|||
onToggleToday,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="task-list-container">
|
||||
{tasks.length > 0 ? (
|
||||
tasks.map((task) => (
|
||||
<TaskItem
|
||||
<div
|
||||
key={task.id}
|
||||
task={task}
|
||||
onTaskUpdate={onTaskUpdate}
|
||||
onTaskDelete={onTaskDelete}
|
||||
projects={projects}
|
||||
hideProjectName={hideProjectName}
|
||||
onToggleToday={onToggleToday}
|
||||
/>
|
||||
className="task-item-wrapper transition-all duration-200 ease-in-out"
|
||||
>
|
||||
<TaskItem
|
||||
task={task}
|
||||
onTaskUpdate={onTaskUpdate}
|
||||
onTaskDelete={onTaskDelete}
|
||||
projects={projects}
|
||||
hideProjectName={hideProjectName}
|
||||
onToggleToday={onToggleToday}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<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
|
||||
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 (
|
||||
<>
|
||||
<div className="flex justify-center items-center mt-4">
|
||||
|
|
@ -52,7 +78,7 @@ const TodayPlan: React.FC<TodayPlanProps> = ({
|
|||
return (
|
||||
<>
|
||||
<TaskList
|
||||
tasks={safeTodayPlanTasks}
|
||||
tasks={sortedTasks}
|
||||
onTaskUpdate={onTaskUpdate}
|
||||
onTaskDelete={onTaskDelete}
|
||||
projects={projects}
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ export const useStore = create<StoreState>((set) => ({
|
|||
set((state) => ({
|
||||
inboxStore: {
|
||||
...state.inboxStore,
|
||||
inboxItems: [...state.inboxStore.inboxItems, inboxItem],
|
||||
inboxItems: [inboxItem, ...state.inboxStore.inboxItems],
|
||||
},
|
||||
})),
|
||||
updateInboxItem: (inboxItem) =>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"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.",
|
||||
"main": "backend/app.js",
|
||||
"directories": {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue