diff --git a/cmd/entries/edit.go b/cmd/entries/edit.go index 9d79c9d..4f7c492 100644 --- a/cmd/entries/edit.go +++ b/cmd/entries/edit.go @@ -433,6 +433,13 @@ func EditCmd() *cobra.Command { os.Exit(1) } + db.SaveLastAction(storage.UndoAction{ + Type: storage.ActionEdit, + EntryID: selectedEntry.ID, + ProjectName: selectedEntry.ProjectName, + Entry: selectedEntry, + }) + fmt.Println() ui.PrintSuccess(ui.EmojiSuccess, "Entry updated successfully") ui.NewlineBelow() diff --git a/cmd/utilities/undo.go b/cmd/utilities/undo.go index 7894220..cefbc44 100644 --- a/cmd/utilities/undo.go +++ b/cmd/utilities/undo.go @@ -17,6 +17,7 @@ var actionDescriptions = map[storage.ActionType]string{ storage.ActionResume: "Resumed tracking", storage.ActionManual: "Created manual entry for", storage.ActionDelete: "Deleted entry for", + storage.ActionEdit: "Edited entry for", } func UndoCmd() *cobra.Command { @@ -104,6 +105,12 @@ func applyUndo(db *storage.Database, action *storage.UndoAction) error { } return db.RestoreDeletedEntry(action.Entry) + case storage.ActionEdit: + if action.Entry == nil { + return fmt.Errorf("no entry snapshot available to restore") + } + return db.UpdateTimeEntry(action.EntryID, action.Entry) + default: return fmt.Errorf("unknown action type: %s", action.Type) } diff --git a/cmd/utilities/undo_test.go b/cmd/utilities/undo_test.go index 3993f4d..8ad6d4f 100644 --- a/cmd/utilities/undo_test.go +++ b/cmd/utilities/undo_test.go @@ -2,6 +2,7 @@ package utilities import ( "testing" + "time" "github.com/DylanDevelops/tmpo/internal/storage" "github.com/stretchr/testify/assert" @@ -31,6 +32,7 @@ func TestUndoActionDescription(t *testing.T) { {storage.ActionResume, "Resumed tracking"}, {storage.ActionManual, "Created manual entry for"}, {storage.ActionDelete, "Deleted entry for"}, + {storage.ActionEdit, "Edited entry for"}, } for _, tt := range tests { @@ -91,6 +93,47 @@ func TestApplyUndo_Delete_ErrorWhenNoSnapshot(t *testing.T) { assert.Contains(t, err.Error(), "no entry snapshot") } +func TestApplyUndo_Edit_ErrorWhenNoSnapshot(t *testing.T) { + db := setupUndoTestDB(t) + + action := &storage.UndoAction{Type: storage.ActionEdit, ProjectName: "proj", Entry: nil} + err := applyUndo(db, action) + assert.Error(t, err) + assert.Contains(t, err.Error(), "no entry snapshot") +} + +func TestApplyUndo_Edit_RestoresOriginalEntry(t *testing.T) { + db := setupUndoTestDB(t) + + // Create and stop an entry, then capture its pre-edit snapshot. + orig, err := db.CreateEntry("proj", "before", nil, nil) + require.NoError(t, err) + require.NoError(t, db.StopEntry(orig.ID)) + + before, err := db.GetEntry(orig.ID) + require.NoError(t, err) + + // Apply an edit that changes several fields. + edited := *before + newStart := before.StartTime.Add(-time.Hour) + newEnd := before.EndTime.Add(time.Hour) + edited.StartTime = newStart + edited.EndTime = &newEnd + edited.Description = "after" + require.NoError(t, db.UpdateTimeEntry(edited.ID, &edited)) + + // Undo should restore the original values. + action := &storage.UndoAction{Type: storage.ActionEdit, EntryID: before.ID, ProjectName: "proj", Entry: before} + require.NoError(t, applyUndo(db, action)) + + restored, err := db.GetEntry(orig.ID) + require.NoError(t, err) + assert.Equal(t, "before", restored.Description) + assert.True(t, restored.StartTime.Equal(before.StartTime), "start time should be restored") + require.NotNil(t, restored.EndTime) + assert.True(t, restored.EndTime.Equal(*before.EndTime), "end time should be restored") +} + func TestApplyUndo_UnknownType_ReturnsError(t *testing.T) { db := setupUndoTestDB(t) diff --git a/docs/usage.md b/docs/usage.md index dcd73f8..a850089 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -433,14 +433,15 @@ Revert the most recent time-tracking action in case of a mistake. A single confi **Supported actions:** -| Action | What undo does | -|---------------|---------------------------------------------------| -| `tmpo start` | Deletes the entry that was just created | -| `tmpo stop` | Re-opens the stopped entry as a running timer | -| `tmpo pause` | Re-opens the paused entry as a running timer | -| `tmpo resume` | Deletes the new entry that was created to resume | -| `tmpo manual` | Deletes the manually created entry | -| `tmpo delete` | Restores the deleted entry with its original data | +| Action | What undo does | +|---------------|-------------------------------------------------------| +| `tmpo start` | Deletes the entry that was just created | +| `tmpo stop` | Re-opens the stopped entry as a running timer | +| `tmpo pause` | Re-opens the paused entry as a running timer | +| `tmpo resume` | Deletes the new entry that was created to resume | +| `tmpo manual` | Deletes the manually created entry | +| `tmpo delete` | Restores the deleted entry with its original data | +| `tmpo edit` | Restores the previous version of an edited time entry | **Examples:** diff --git a/internal/storage/undo.go b/internal/storage/undo.go index 8c0af86..e40296a 100644 --- a/internal/storage/undo.go +++ b/internal/storage/undo.go @@ -16,6 +16,7 @@ const ( ActionResume ActionType = "resume" ActionDelete ActionType = "delete" ActionManual ActionType = "manual" + ActionEdit ActionType = "edit" ) const lastActionKey = "last_action"