Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Unlimotion.Domain/TaskItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public record TaskItem
public bool? IsCompleted { get; set; } = false;
public bool IsCanBeCompleted { get; set; } = true;
public DateTimeOffset CreatedDateTime { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? UpdatedDateTime { get; set; }
public DateTimeOffset? UnlockedDateTime { get; set; }
public DateTimeOffset? CompletedDateTime { get; set; }
public DateTimeOffset? ArchiveDateTime { get; set; }
Expand Down
1 change: 1 addition & 0 deletions src/Unlimotion.Interface/ReceiveTaskItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class ReceiveTaskItem : IClientMethod
public bool? IsCompleted { get; set; } = false;
public bool IsCanBeCompleted { get; set; } = false;
public DateTimeOffset CreatedDateTime { get; set; }
public DateTimeOffset? UpdatedDateTime { get; set; }
public DateTimeOffset? UnlockedDateTime { get; set; }
public DateTimeOffset? CompletedDateTime { get; set; }
public DateTimeOffset? ArchiveDateTime { get; set; }
Expand Down
1 change: 1 addition & 0 deletions src/Unlimotion.Interface/TaskItemHubMold.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class TaskItemHubMold
public string Title { get; set; }
public string Description { get; set; }
public bool? IsCompleted { get; set; } = false;
public DateTimeOffset? UpdatedDateTime { get; set; }
public DateTimeOffset? UnlockedDateTime { get; set; }
public DateTimeOffset? CompletedDateTime { get; set; }
public DateTimeOffset? ArchiveDateTime { get; set; }
Expand Down
Binary file modified src/Unlimotion.Server.ServiceModel/molds/Tasks/TaskItemMold.cs
Binary file not shown.
17 changes: 17 additions & 0 deletions src/Unlimotion.TaskTreeManager/TaskTreeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ await IsCompletedAsync(async Task<bool> () =>
try
{
change.Version = 1;
change.UpdatedDateTime ??= change.CreatedDateTime;
await Storage.Save(change);
result.AddOrUpdate(change);

Expand All @@ -53,6 +54,7 @@ await IsCompletedAsync(async Task<bool> () =>
if (newTaskId is null)
{
change.Version = 1;
change.UpdatedDateTime ??= change.CreatedDateTime;
await Storage.Save(change);
newTaskId = change.Id;
result.AddOrUpdate(change);
Expand Down Expand Up @@ -104,6 +106,7 @@ await IsCompletedAsync(async Task<bool> () =>
if (newTaskId is null)
{
change.Version = 1;
change.UpdatedDateTime ??= change.CreatedDateTime;
await Storage.Save(change);
newTaskId = change.Id;
result.AddOrUpdate(change);
Expand Down Expand Up @@ -301,6 +304,7 @@ await IsCompletedAsync(async Task<bool> () =>
else
{
// Regular update without IsCompleted change
change.UpdatedDateTime = GetNextUpdatedDateTime(change);
await Storage.Save(change);
result.AddOrUpdate(change);
}
Expand Down Expand Up @@ -890,6 +894,7 @@ await IsCompletedAsync(async () =>

// Save the cloned task
clone.Version = 1;
clone.UpdatedDateTime ??= clone.CreatedDateTime;
await Storage.Save(clone);
result.AddOrUpdate(clone);

Expand Down Expand Up @@ -952,6 +957,7 @@ await IsCompletedAsync(async () =>
}

// Save the updated task
task.UpdatedDateTime = GetNextUpdatedDateTime(task);
await Storage.Save(task);
result.AddOrUpdate(task);

Expand All @@ -968,4 +974,15 @@ await IsCompletedAsync(async () =>

return result.Values.ToList();
}

private static DateTimeOffset GetNextUpdatedDateTime(TaskItem task)
{
var now = DateTimeOffset.Now;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate UpdatedDateTime in UTC to keep date filters correct

GetNextUpdatedDateTime now uses DateTimeOffset.Now, while other task timestamps are written in UTC and the date filters normalize by adding the local offset. Storing this field with local offset makes that normalization apply twice in non-UTC environments, which can shift updates across day boundaries (especially near midnight) and misclassify tasks in the Last Updated date filters.

Useful? React with 👍 / 👎.

if (task.UpdatedDateTime.HasValue && now <= task.UpdatedDateTime.Value)
{
return task.UpdatedDateTime.Value.AddSeconds(1);
}

return now;
}
}
2 changes: 1 addition & 1 deletion src/Unlimotion.TelegramBot/Bot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ private static async Task ShowTask(long chatId, TaskItemViewModel task)
string response = $"{(task.IsCanBeCompleted?"":"🔒")}{GetStatusEmodji(task.IsCompleted)} {(task.Wanted?"*":"")}{task.Title}{(task.Wanted?"*":"")}\n" +
$"{GetStatusEmodji(task.Wanted)} Wanted | Importance {task.Importance}\nId {task.Id}\n" +
$"{task.Description}\n" +
$"Created {task.CreatedDateTime:yyyy.MM.dd HH:mm} Unlocked {task.UnlockedDateTime:yyyy.MM.dd HH:mm} Completed {task.CompletedDateTime:yyyy.MM.dd HH:mm} Archive {task.ArchiveDateTime:yyyy.MM.dd HH:mm}\n" +
$"Created {task.CreatedDateTime:yyyy.MM.dd HH:mm} Updated {task.UpdatedDateTime:yyyy.MM.dd HH:mm} Unlocked {task.UnlockedDateTime:yyyy.MM.dd HH:mm} Completed {task.CompletedDateTime:yyyy.MM.dd HH:mm} Archive {task.ArchiveDateTime:yyyy.MM.dd HH:mm}\n" +
$"Begin {task.PlannedBeginDateTime:yyyy.MM.dd} Duration {TimeSpanStringConverter.SpanToString(task.PlannedDuration)} End {task.PlannedEndDateTime:yyyy.MM.dd}\n"
;

Expand Down
1 change: 1 addition & 0 deletions src/Unlimotion.Test/InMemoryStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public async Task<TaskItem> Save(TaskItem taskItem)
IsCompleted = taskItem.IsCompleted,
IsCanBeCompleted = taskItem.IsCanBeCompleted,
CreatedDateTime = taskItem.CreatedDateTime,
UpdatedDateTime = taskItem.UpdatedDateTime,
UnlockedDateTime = taskItem.UnlockedDateTime,
CompletedDateTime = taskItem.CompletedDateTime,
ArchiveDateTime = taskItem.ArchiveDateTime,
Expand Down
27 changes: 17 additions & 10 deletions src/Unlimotion.Test/MainWindowViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public async Task RenameTask_Success()

var after = TestHelpers.GetStorageTaskItem(fixture.DefaultTasksFolderPath, task.Id);
var result = TestHelpers.CompareStorageVersions(before!, after!);
await TestHelpers.ShouldHaveOnlyTitleChanged(result, "Root Task 1", "Changed task title");
await TestHelpers.ShouldHaveTitleAndAUpdatedDateChanged(result, "Root Task 1", "Changed task title");
}

/// <summary>
Expand Down Expand Up @@ -746,17 +746,21 @@ public async Task CompletingBlockingTask_Success()
//У нее проставлено время разблокировки
await Assert.That(blockedTask5AfterTest.UnlockedDateTime).IsNotNull();
var result = compareLogic.Compare(blockedTask5BeforeTest, blockedTask5AfterTest);
//Должно быть два различия: IsCanBeCompleted и UnlockedDateTime
await Assert.That(result.Differences.Count).IsEqualTo(2);
//Должно быть 3 различия: IsCanBeCompleted, UpdatedDateTime и UnlockedDateTime
await Assert.That(result.Differences.Count).IsEqualTo(3);
var isCanBeCompletedDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(blockedTask5AfterTest.IsCanBeCompleted));
var unlockedDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(blockedTask5AfterTest.UnlockedDateTime));
var updatedDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(TaskItem.UpdatedDateTime));

await Assert.That(isCanBeCompletedDifference).IsNotNull();
await Assert.That(isCanBeCompletedDifference.Object1).IsEqualTo(false);
await Assert.That(isCanBeCompletedDifference.Object2).IsEqualTo(true);
await Assert.That(unlockedDateTimeDifference).IsNotNull();
await Assert.That(unlockedDateTimeDifference.Object1).IsNull();
await Assert.That(unlockedDateTimeDifference.Object2).IsNotNull();
await Assert.That(updatedDateTimeDifference).IsNotNull();
await Assert.That(updatedDateTimeDifference.Object1).IsNull();
await Assert.That(updatedDateTimeDifference.Object2).IsNotNull();

var blockedTask5ViewModel = taskRepository.Tasks.Items.First(i => i.Id == MainWindowViewModelFixture.BlockedTask5Id);
await Assert.That(blockedTask5ViewModel).IsNotNull();
Expand All @@ -766,11 +770,11 @@ public async Task CompletingBlockingTask_Success()
var rootTask5AfterTest = GetStorageTaskItem(MainWindowViewModelFixture.RootTask5Id);
//Проверяем, что в блокирующем таске изменилось
result = compareLogic.Compare(blockingTask5BeforeTest, rootTask5AfterTest);
//Должно быть 2 различия поля IsCompleted и CompletedDateTime
await Assert.That(result.Differences.Count).IsEqualTo(2);
//Должно быть 3 различия поля IsCompleted, UpdatedDateTime и CompletedDateTime
await Assert.That(result.Differences.Count).IsEqualTo(3);
var isCompletedDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(TaskItem.IsCompleted));
var completedDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(TaskItem.CompletedDateTime));

await Assert.That(isCompletedDifference).IsNotNull();
await Assert.That(completedDateTimeDifference).IsNotNull();
await Assert.That(isCompletedDifference.Object1).IsEqualTo(false);
Expand Down Expand Up @@ -967,10 +971,11 @@ public async Task CloneTask_Success()
var clonedTask8ItemAfterTest = GetStorageTaskItem(clonedViewModel.Id);
//Сравниваем клонируюмую задачу с новой созданной
result = compareLogic.Compare(clonedTask8ItemAfterTest, newTaskItem);
//Должны отличаться id, дата создания и кол-во родителей
await Assert.That(result.Differences.Count).IsEqualTo(3);
// Должны отличаться минимум id, дата создания, дата обновления и кол-во родителей.
await Assert.That(result.Differences.Count).IsEqualTo(4);
await Assert.That(result.Differences.Select(d => d.PropertyName)).Contains(nameof(TaskItem.Id));
await Assert.That(result.Differences.Select(d => d.PropertyName)).Contains(nameof(TaskItem.CreatedDateTime));
await Assert.That(result.Differences.Select(d => d.PropertyName)).Contains(nameof(TaskItem.UpdatedDateTime));
await Assert.That(result.Differences.Select(d => d.PropertyName)).Contains(nameof(TaskItem.ParentTasks));
var parentTasksDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(TaskItem.ParentTasks));
await Assert.That(((IList)parentTasksDifference.Object1).Count).IsEqualTo(0);
Expand Down Expand Up @@ -1027,16 +1032,18 @@ public async Task CompleteRepeatableTaskTask_Success()
//Сравниваем ее с исходной до выполнения
result = compareLogic.Compare(repeateTask9BeforeTest, newTask9);

//Должно быть только 5 различия: Id, CreatedDateTime, UnlockedDateTime, PlannedBeginDateTime, PlannedEndDateTime
await Assert.That(result.Differences.Count).IsEqualTo(5);
//Должно быть только 6 различий: Id, CreatedDateTime, UpdatedDateTime, UnlockedDateTime, PlannedBeginDateTime, PlannedEndDateTime
await Assert.That(result.Differences.Count).IsEqualTo(6);
var idDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(repeateTask9AfterTest.Id));
var createdDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(repeateTask9AfterTest.CreatedDateTime));
var updatedDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(repeateTask9AfterTest.UpdatedDateTime));
var unlockedDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(repeateTask9AfterTest.UnlockedDateTime));
var plannedBeginDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(repeateTask9AfterTest.PlannedBeginDateTime));
var plannedEndDateTimeDifference = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(repeateTask9AfterTest.PlannedEndDateTime));

await Assert.That(idDifference).IsNotNull();
await Assert.That(createdDateTimeDifference).IsNotNull();
await Assert.That(updatedDateTimeDifference).IsNotNull();
await Assert.That(unlockedDateTimeDifference).IsNotNull();
await Assert.That(plannedBeginDateTimeDifference).IsNotNull();
await Assert.That(plannedEndDateTimeDifference).IsNotNull();
Expand Down
33 changes: 33 additions & 0 deletions src/Unlimotion.Test/TaskCompletionChangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,38 @@ public async Task HandleTaskCompletionChange_CompletedTaskWithRepeater_ShouldSyn
await Assert.That(cloneFromStorage.IsCanBeCompleted).IsFalse();
await Assert.That(cloneFromStorage.UnlockedDateTime).IsNull();
}

[Test]
public async Task HandleTaskCompletionChange_UpdateTask_SetUpdatedDateTime()
{
// Arrange
var storage = new InMemoryStorage();
var manager = new TaskTreeManager(storage);

var task = new TaskItem
{
Id = "test-task",
Title = "v1",
Description = "d1",
IsCompleted = false
};

await storage.Save(task);

// Act 1
task.Title = "v2";
await manager.UpdateTask(task);
var firstUpdated = task.UpdatedDateTime;

// Act 2
task.Description = "d2";
await manager.UpdateTask(task);
var secondUpdated = task.UpdatedDateTime;

// Assert
await Assert.That(firstUpdated).IsNotNull();
await Assert.That(secondUpdated).IsNotNull();
await Assert.That(secondUpdated > firstUpdated).IsTrue();
}
}
}
13 changes: 9 additions & 4 deletions src/Unlimotion.Test/TestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,16 @@ public static ComparisonResult CompareStorageVersions(TaskItem before, TaskItem
return compareLogic.Compare(before, after);
}

public static async Task ShouldHaveOnlyTitleChanged(ComparisonResult result, string oldTitle, string newTitle)
public static async Task ShouldHaveTitleAndAUpdatedDateChanged(ComparisonResult result, string oldTitle, string newTitle)
{
await Assert.That(result.Differences).HasSingleItem();
await Assert.That(result.Differences[0].PropertyName).IsEqualTo(nameof(TaskItem.Title));
await Assert.That(result.DifferencesString).StartsWith($"\r\nBegin Differences (1 differences):\r\nTypes [String,String], Item Expected.Title != Actual.Title, Values ({oldTitle},{newTitle})");
var names = result.Differences.Select(d => d.PropertyName).ToList();
var titleDiff = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(TaskItem.Title));
var updatedDateDiff = result.Differences.FirstOrDefault(d => d.PropertyName == nameof(TaskItem.UpdatedDateTime));
await Assert.That(titleDiff).IsNotNull();
await Assert.That(updatedDateDiff).IsNotNull();
await Assert.That((titleDiff.Object1 ?? "").ToString()).IsEqualTo(oldTitle);
await Assert.That((titleDiff.Object2 ?? "").ToString()).IsEqualTo(newTitle);
await Assert.That(updatedDateDiff.Object1).IsNotEqualTo(updatedDateDiff.Object2);
}

public static async Task ShouldContainOnlyDifference(ComparisonResult result, string propertyName)
Expand Down
5 changes: 4 additions & 1 deletion src/Unlimotion.ViewModel/FileDbWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ public void SetEnable(bool enable)
lock (itLockEnable)
{
isEnable = enable;
watcher?.EnableRaisingEvents = enable;
if (watcher != null)
{
watcher.EnableRaisingEvents = enable;
}
}
}

Expand Down
Loading
Loading