mirror of
https://github.com/aculix/negotium.git
synced 2026-09-11 07:28:17 +00:00
Compare commits
1 Commits
v2.2.0
...
7f1a02ee41
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f1a02ee41 |
@@ -14,7 +14,7 @@ Built with Svelte for speed and simplicity. No overwhelming features, no endless
|
|||||||
|
|
||||||
### Core Functionality
|
### Core Functionality
|
||||||
- ✅ **Add, complete, and delete tasks** with smooth animations
|
- ✅ **Add, complete, and delete tasks** with smooth animations
|
||||||
- ↩️ **Undo** - Deleted something by mistake? An Undo button appears, and `Cmd/Ctrl+Z` works too. The task returns to its original position
|
- ↩️ **Undo** - Deleted or moved something by mistake? An Undo button appears, and `Cmd/Ctrl+Z` works too. The task returns to its original position
|
||||||
- 📅 **Today & Tomorrow lists** - Plan ahead with separate task lists
|
- 📅 **Today & Tomorrow lists** - Plan ahead with separate task lists
|
||||||
- 🔄 **Unfinished work carries over** - When a new day begins, tasks you didn't finish move to Today; completed ones are cleared away
|
- 🔄 **Unfinished work carries over** - When a new day begins, tasks you didn't finish move to Today; completed ones are cleared away
|
||||||
- ✏️ **Edit a task** - Fix a typo without deleting and retyping it
|
- ✏️ **Edit a task** - Fix a typo without deleting and retyping it
|
||||||
@@ -92,7 +92,7 @@ The date, storage, rollover, task and undo logic lives in `src/lib/` as plain mo
|
|||||||
- **Complete a task**: Click the checkbox next to the task
|
- **Complete a task**: Click the checkbox next to the task
|
||||||
- **Edit a task**: Click its text. Enter saves, Escape cancels, and clicking away saves too
|
- **Edit a task**: Click its text. Enter saves, Escape cancels, and clicking away saves too
|
||||||
- **Delete a task**: Click the delete icon on the task (it shows on hover, and is always visible on touch), or press `Delete` with the task focused
|
- **Delete a task**: Click the delete icon on the task (it shows on hover, and is always visible on touch), or press `Delete` with the task focused
|
||||||
- **Undo a delete**: An Undo button appears for a few seconds after anything is removed. `Cmd/Ctrl+Z` does the same. Either way the task returns to where it was
|
- **Undo**: An Undo button appears for a few seconds after a task is deleted, cleared or moved to another day. `Cmd/Ctrl+Z` does the same. Either way the task returns to where it was
|
||||||
- **Move a task to Tomorrow**: Click the arrow on the task, or press `Alt+→`. From Tomorrow, the arrow points back and `Alt+←` returns it to Today
|
- **Move a task to Tomorrow**: Click the arrow on the task, or press `Alt+→`. From Tomorrow, the arrow points back and `Alt+←` returns it to Today
|
||||||
- **Reorder tasks**: Drag with a mouse, long-press then drag on touch, or focus a task and press `Alt+↑`/`Alt+↓`
|
- **Reorder tasks**: Drag with a mouse, long-press then drag on touch, or focus a task and press `Alt+↑`/`Alt+↓`
|
||||||
- **Clear input**: Press Escape while the input is focused
|
- **Clear input**: Press Escape while the input is focused
|
||||||
@@ -130,7 +130,7 @@ Import only ever adds. Tasks already present are left alone, so importing the sa
|
|||||||
- **Escape**: Clear input field (when input is focused)
|
- **Escape**: Clear input field (when input is focused)
|
||||||
- **Space/Enter**: Toggle task completion (when a task's checkbox is focused)
|
- **Space/Enter**: Toggle task completion (when a task's checkbox is focused)
|
||||||
- **Delete**: Delete the focused task
|
- **Delete**: Delete the focused task
|
||||||
- **Cmd/Ctrl+Z**: Undo the last delete or clear-completed
|
- **Cmd/Ctrl+Z**: Undo the last delete, clear-completed, or move between days
|
||||||
- **Alt+↑ / Alt+↓**: Move the focused task up or down
|
- **Alt+↑ / Alt+↓**: Move the focused task up or down
|
||||||
- **Alt+→ / Alt+←**: Send the focused task to Tomorrow, or bring it back to Today
|
- **Alt+→ / Alt+←**: Send the focused task to Tomorrow, or bring it back to Today
|
||||||
|
|
||||||
|
|||||||
+27
-3
@@ -139,8 +139,21 @@
|
|||||||
* which day you are looking at, so one control covers both. */
|
* which day you are looking at, so one control covers both. */
|
||||||
function deferTask(taskId) {
|
function deferTask(taskId) {
|
||||||
cancelEdit();
|
cancelEdit();
|
||||||
const destination = viewingToday ? tomorrowKey() : todayKey;
|
|
||||||
tasks = moveTaskToDay(storage, selectedKey, destination, taskId);
|
const index = tasks.findIndex(task => task.id === taskId);
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const movingToTomorrow = viewingToday;
|
||||||
|
const from = selectedKey;
|
||||||
|
const to = movingToTomorrow ? tomorrowKey() : todayKey;
|
||||||
|
|
||||||
|
tasks = moveTaskToDay(storage, from, to, taskId);
|
||||||
|
|
||||||
|
// A task vanishing from the list with nothing said about it reads as a
|
||||||
|
// delete, especially right after the app has taught you that deletes come
|
||||||
|
// with a way back. It gets the same treatment.
|
||||||
|
undoStack.push({ type: 'move', id: taskId, from, to, index });
|
||||||
|
showUndo(movingToTomorrow ? 'Moved to Tomorrow' : 'Moved to Today');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,8 +183,19 @@
|
|||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
const entry = undoStack.pop();
|
const entry = undoStack.pop();
|
||||||
if (entry) setTasks(applyUndo(tasks, entry));
|
|
||||||
dismissUndo();
|
dismissUndo();
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
// A move spans two days, so it cannot be expressed as an edit to the one
|
||||||
|
// list applyUndo works on. It is sent back to the row it came from, which
|
||||||
|
// is why the index was recorded.
|
||||||
|
if (entry.type === 'move') {
|
||||||
|
moveTaskToDay(storage, entry.to, entry.from, entry.id, entry.index);
|
||||||
|
tasks = storage.loadTasks(selectedKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTasks(applyUndo(tasks, entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
|
|||||||
+12
-2
@@ -9,8 +9,12 @@
|
|||||||
* A task already sitting at the destination is not duplicated. It still leaves
|
* A task already sitting at the destination is not duplicated. It still leaves
|
||||||
* the source day, which is what someone dragging a stray copy around would
|
* the source day, which is what someone dragging a stray copy around would
|
||||||
* expect.
|
* expect.
|
||||||
|
*
|
||||||
|
* `insertAt` places the task at a position rather than at the end. Undoing a
|
||||||
|
* move needs it: a task sent to Tomorrow should come back to the row it left,
|
||||||
|
* not to the bottom of the list.
|
||||||
*/
|
*/
|
||||||
export function moveTaskToDay(storage, fromKey, toKey, taskId) {
|
export function moveTaskToDay(storage, fromKey, toKey, taskId, insertAt = null) {
|
||||||
const source = storage.loadTasks(fromKey)
|
const source = storage.loadTasks(fromKey)
|
||||||
if (fromKey === toKey) return source
|
if (fromKey === toKey) return source
|
||||||
|
|
||||||
@@ -22,7 +26,13 @@ export function moveTaskToDay(storage, fromKey, toKey, taskId) {
|
|||||||
const alreadyThere = destination.some(task => task.id === taskId)
|
const alreadyThere = destination.some(task => task.id === taskId)
|
||||||
|
|
||||||
storage.saveTasks(fromKey, remaining)
|
storage.saveTasks(fromKey, remaining)
|
||||||
if (!alreadyThere) storage.saveTasks(toKey, [...destination, moving])
|
|
||||||
|
if (!alreadyThere) {
|
||||||
|
const next = [...destination]
|
||||||
|
const at = insertAt === null ? next.length : Math.max(0, Math.min(insertAt, next.length))
|
||||||
|
next.splice(at, 0, moving)
|
||||||
|
storage.saveTasks(toKey, next)
|
||||||
|
}
|
||||||
|
|
||||||
return remaining
|
return remaining
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,3 +81,39 @@ describe('moveTaskToDay', () => {
|
|||||||
expect(storage.loadTasks(TODAY)).toEqual([])
|
expect(storage.loadTasks(TODAY)).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('moveTaskToDay with an insert position', () => {
|
||||||
|
it('inserts at the given index rather than appending', () => {
|
||||||
|
const storage = setup({ [TOMORROW]: [task('x')], [TODAY]: [task('a'), task('b'), task('c')] })
|
||||||
|
moveTaskToDay(storage, TOMORROW, TODAY, 'x', 1)
|
||||||
|
expect(storage.loadTasks(TODAY).map(t => t.id)).toEqual(['a', 'x', 'b', 'c'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('puts a task back at the front', () => {
|
||||||
|
const storage = setup({ [TOMORROW]: [task('x')], [TODAY]: [task('a')] })
|
||||||
|
moveTaskToDay(storage, TOMORROW, TODAY, 'x', 0)
|
||||||
|
expect(storage.loadTasks(TODAY).map(t => t.id)).toEqual(['x', 'a'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clamps an index past the end', () => {
|
||||||
|
const storage = setup({ [TOMORROW]: [task('x')], [TODAY]: [task('a')] })
|
||||||
|
moveTaskToDay(storage, TOMORROW, TODAY, 'x', 99)
|
||||||
|
expect(storage.loadTasks(TODAY).map(t => t.id)).toEqual(['a', 'x'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still appends when no index is given', () => {
|
||||||
|
const storage = setup({ [TOMORROW]: [task('x')], [TODAY]: [task('a')] })
|
||||||
|
moveTaskToDay(storage, TOMORROW, TODAY, 'x')
|
||||||
|
expect(storage.loadTasks(TODAY).map(t => t.id)).toEqual(['a', 'x'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('round-trips a defer back to where it started', () => {
|
||||||
|
const storage = setup({ [TODAY]: [task('a'), task('b'), task('c')] })
|
||||||
|
moveTaskToDay(storage, TODAY, TOMORROW, 'b')
|
||||||
|
expect(storage.loadTasks(TODAY).map(t => t.id)).toEqual(['a', 'c'])
|
||||||
|
|
||||||
|
moveTaskToDay(storage, TOMORROW, TODAY, 'b', 1)
|
||||||
|
expect(storage.loadTasks(TODAY).map(t => t.id)).toEqual(['a', 'b', 'c'])
|
||||||
|
expect(storage.loadTasks(TOMORROW)).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user