diff --git a/src/Unlimotion.Domain/TaskItem.cs b/src/Unlimotion.Domain/TaskItem.cs index 948248b..060b0e3 100644 --- a/src/Unlimotion.Domain/TaskItem.cs +++ b/src/Unlimotion.Domain/TaskItem.cs @@ -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; } diff --git a/src/Unlimotion.Interface/ReceiveTaskItem.cs b/src/Unlimotion.Interface/ReceiveTaskItem.cs index 3bf2aaa..3265b4e 100644 --- a/src/Unlimotion.Interface/ReceiveTaskItem.cs +++ b/src/Unlimotion.Interface/ReceiveTaskItem.cs @@ -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; } diff --git a/src/Unlimotion.Interface/TaskItemHubMold.cs b/src/Unlimotion.Interface/TaskItemHubMold.cs index aa499d2..853eafa 100644 --- a/src/Unlimotion.Interface/TaskItemHubMold.cs +++ b/src/Unlimotion.Interface/TaskItemHubMold.cs @@ -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; } diff --git a/src/Unlimotion.Server.ServiceModel/molds/Tasks/TaskItemMold.cs b/src/Unlimotion.Server.ServiceModel/molds/Tasks/TaskItemMold.cs index 330ba26..dbdcd50 100644 Binary files a/src/Unlimotion.Server.ServiceModel/molds/Tasks/TaskItemMold.cs and b/src/Unlimotion.Server.ServiceModel/molds/Tasks/TaskItemMold.cs differ diff --git a/src/Unlimotion.TaskTreeManager/TaskTreeManager.cs b/src/Unlimotion.TaskTreeManager/TaskTreeManager.cs index c691391..af46514 100644 --- a/src/Unlimotion.TaskTreeManager/TaskTreeManager.cs +++ b/src/Unlimotion.TaskTreeManager/TaskTreeManager.cs @@ -29,6 +29,7 @@ await IsCompletedAsync(async Task () => try { change.Version = 1; + change.UpdatedDateTime ??= change.CreatedDateTime; await Storage.Save(change); result.AddOrUpdate(change); @@ -53,6 +54,7 @@ await IsCompletedAsync(async Task () => if (newTaskId is null) { change.Version = 1; + change.UpdatedDateTime ??= change.CreatedDateTime; await Storage.Save(change); newTaskId = change.Id; result.AddOrUpdate(change); @@ -104,6 +106,7 @@ await IsCompletedAsync(async Task () => if (newTaskId is null) { change.Version = 1; + change.UpdatedDateTime ??= change.CreatedDateTime; await Storage.Save(change); newTaskId = change.Id; result.AddOrUpdate(change); @@ -301,6 +304,7 @@ await IsCompletedAsync(async Task () => else { // Regular update without IsCompleted change + change.UpdatedDateTime = GetNextUpdatedDateTime(change); await Storage.Save(change); result.AddOrUpdate(change); } @@ -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); @@ -952,6 +957,7 @@ await IsCompletedAsync(async () => } // Save the updated task + task.UpdatedDateTime = GetNextUpdatedDateTime(task); await Storage.Save(task); result.AddOrUpdate(task); @@ -968,4 +974,15 @@ await IsCompletedAsync(async () => return result.Values.ToList(); } + + private static DateTimeOffset GetNextUpdatedDateTime(TaskItem task) + { + var now = DateTimeOffset.Now; + if (task.UpdatedDateTime.HasValue && now <= task.UpdatedDateTime.Value) + { + return task.UpdatedDateTime.Value.AddSeconds(1); + } + + return now; + } } diff --git a/src/Unlimotion.TelegramBot/Bot.cs b/src/Unlimotion.TelegramBot/Bot.cs index 5898d32..2a48ad1 100644 --- a/src/Unlimotion.TelegramBot/Bot.cs +++ b/src/Unlimotion.TelegramBot/Bot.cs @@ -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" ; diff --git a/src/Unlimotion.Test/InMemoryStorage.cs b/src/Unlimotion.Test/InMemoryStorage.cs index 69d1974..2c33aec 100644 --- a/src/Unlimotion.Test/InMemoryStorage.cs +++ b/src/Unlimotion.Test/InMemoryStorage.cs @@ -55,6 +55,7 @@ public async Task Save(TaskItem taskItem) IsCompleted = taskItem.IsCompleted, IsCanBeCompleted = taskItem.IsCanBeCompleted, CreatedDateTime = taskItem.CreatedDateTime, + UpdatedDateTime = taskItem.UpdatedDateTime, UnlockedDateTime = taskItem.UnlockedDateTime, CompletedDateTime = taskItem.CompletedDateTime, ArchiveDateTime = taskItem.ArchiveDateTime, diff --git a/src/Unlimotion.Test/MainWindowViewModelTests.cs b/src/Unlimotion.Test/MainWindowViewModelTests.cs index 6e8324b..03bdd3e 100644 --- a/src/Unlimotion.Test/MainWindowViewModelTests.cs +++ b/src/Unlimotion.Test/MainWindowViewModelTests.cs @@ -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"); } /// @@ -746,10 +746,11 @@ 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); @@ -757,6 +758,9 @@ public async Task CompletingBlockingTask_Success() 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(); @@ -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); @@ -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); @@ -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(); diff --git a/src/Unlimotion.Test/TaskCompletionChangeTests.cs b/src/Unlimotion.Test/TaskCompletionChangeTests.cs index 2865055..819716e 100644 --- a/src/Unlimotion.Test/TaskCompletionChangeTests.cs +++ b/src/Unlimotion.Test/TaskCompletionChangeTests.cs @@ -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(); + } } } \ No newline at end of file diff --git a/src/Unlimotion.Test/TestHelpers.cs b/src/Unlimotion.Test/TestHelpers.cs index 677c8fb..e306d00 100644 --- a/src/Unlimotion.Test/TestHelpers.cs +++ b/src/Unlimotion.Test/TestHelpers.cs @@ -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) diff --git a/src/Unlimotion.ViewModel/FileDbWatcher.cs b/src/Unlimotion.ViewModel/FileDbWatcher.cs index 1e6e2b5..a346576 100644 --- a/src/Unlimotion.ViewModel/FileDbWatcher.cs +++ b/src/Unlimotion.ViewModel/FileDbWatcher.cs @@ -27,7 +27,10 @@ public void SetEnable(bool enable) lock (itLockEnable) { isEnable = enable; - watcher?.EnableRaisingEvents = enable; + if (watcher != null) + { + watcher.EnableRaisingEvents = enable; + } } } diff --git a/src/Unlimotion.ViewModel/MainWindowViewModel.cs b/src/Unlimotion.ViewModel/MainWindowViewModel.cs index aeb9651..cfe1f07 100644 --- a/src/Unlimotion.ViewModel/MainWindowViewModel.cs +++ b/src/Unlimotion.ViewModel/MainWindowViewModel.cs @@ -21,6 +21,7 @@ public class MainWindowViewModel : DisposableList private bool _isCompletedTabInitialized; private bool _isArchivedTabInitialized; private bool _isLastCreatedTabInitialized; + private bool _isLastUpdatedTabInitialized; private bool _isRoadmapTabInitialized; private bool _isUnlockedTabInitialized; private bool _isLastOpenedTabInitialized; @@ -50,6 +51,7 @@ public MainWindowViewModel( CompletedItems = EmptyTaskWrappers; ArchivedItems = EmptyTaskWrappers; LastCreatedItems = EmptyTaskWrappers; + LastUpdatedItems = EmptyTaskWrappers; LastOpenedItems = EmptyTaskWrappers; Graph.SetMainWindowViewModel(this); Graph.Search = Search; @@ -191,6 +193,14 @@ private void RegisterCommands() }) .AddToDispose(connectionDisposableList); + this.WhenAnyValue(m => m.CurrentLastUpdated) + .Subscribe(m => + { + if (m != null && CurrentTaskItem != m?.TaskItem) + CurrentTaskItem = m?.TaskItem; + }) + .AddToDispose(connectionDisposableList); + this.WhenAnyValue(m => m.CurrentLastOpenedItem) .Subscribe(m => { @@ -199,9 +209,17 @@ private void RegisterCommands() }) .AddToDispose(connectionDisposableList); - this.WhenAnyValue(m => m.AllTasksMode, m => m.UnlockedMode, m => m.CompletedMode, m => m.ArchivedMode, m => m.GraphMode, m => m.LastCreatedMode, m => m.LastOpenedMode) - .Subscribe(a => - { + Observable.Merge( + this.WhenAnyValue(m => m.AllTasksMode), + this.WhenAnyValue(m => m.UnlockedMode), + this.WhenAnyValue(m => m.CompletedMode), + this.WhenAnyValue(m => m.ArchivedMode), + this.WhenAnyValue(m => m.GraphMode), + this.WhenAnyValue(m => m.LastCreatedMode), + this.WhenAnyValue(m => m.LastUpdatedMode), + this.WhenAnyValue(m => m.LastOpenedMode)) + .Subscribe(_ => + { SelectCurrentTask(); }) .AddToDispose(connectionDisposableList); @@ -226,6 +244,7 @@ public async Task Connect() _isCompletedTabInitialized = false; _isArchivedTabInitialized = false; _isLastCreatedTabInitialized = false; + _isLastUpdatedTabInitialized = false; _isRoadmapTabInitialized = false; _isUnlockedTabInitialized = false; _isLastOpenedTabInitialized = false; @@ -676,8 +695,7 @@ bool Predicate(TaskItemViewModel task) }); #endregion Unlocked - - //Bind Completed + this.WhenAnyValue(m => m.LastCreatedDateFilter.CurrentOption, m => m.LastCreatedDateFilter.IsCustom) .Subscribe(filter => { @@ -702,6 +720,31 @@ bool Predicate(TaskItemViewModel task) return (Func)Predicate; }); + this.WhenAnyValue(m => m.LastUpdatedDateFilter.CurrentOption, m => m.LastUpdatedDateFilter.IsCustom) + .Subscribe(filter => + { + if (!filter.Item2) + LastUpdatedDateFilter.SetDateTimes(filter.Item1); + }); + + var lastUpdatedDateFilter = this.WhenAnyValue(m => m.LastUpdatedDateFilter.From, + m => m.LastUpdatedDateFilter.To, m => m.LastUpdatedDateFilter.IsCustom) + .Select(filter => + { + bool Predicate(TaskItemViewModel task) + { + if (filter.Item1 == null || filter.Item2 == null) + return true; + + var dateTime = task.UpdatedDateTime?.Add(DateTimeOffset.Now.Offset).Date; + return filter.Item1 <= dateTime && dateTime <= filter.Item2; + } + + return (Func)Predicate; + }); + + //Bind Completed + void ActivateCompletedProjection() { if (_isCompletedTabInitialized) @@ -820,6 +863,46 @@ void ActivateLastCreatedProjection() LastCreatedItems = _lastCreatedItems; } + void ActivateLastUpdatedProjection() + { + if (_isLastUpdatedTabInitialized) + { + return; + } + + _isLastUpdatedTabInitialized = true; + taskRepository.Tasks + .Connect() + .AutoRefreshOnObservable(m => m.WhenAny(m => m.IsCanBeCompleted, m => m.IsCompleted, + m => m.UnlockedDateTime, (c, d, u) => c.Value && (d.Value == false))) + .AutoRefreshOnObservable(m => m.WhenAnyValue( + x => x.Title, + x => x.Description, + x => x.GetAllEmoji)) + .Filter(taskFilter) + .Filter(lastUpdatedDateFilter) + .Filter(emojiFilter) + .Filter(emojiExcludeFilter) + .Filter(searchTopFilter) + .Transform(item => + { + var actions = new TaskWrapperActions + { + ChildSelector = m => m.ContainsTasks.ToObservableChangeSet(), + RemoveAction = RemoveTask, + GetBreadScrumbs = BredScrumbsAlgorithms.FirstTaskParent, + }; + var wrapper = new TaskWrapperViewModel(null, item, actions); + return wrapper; + }) + .SortBy(e => e.TaskItem.UpdatedDateTime, SortDirection.Descending) + .Bind(out _lastUpdatedItems) + .Subscribe() + .AddToDispose(connectionDisposableList); + + LastUpdatedItems = _lastUpdatedItems; + } + void ActivateRoadmapProjection() { if (_isRoadmapTabInitialized) @@ -902,6 +985,12 @@ void ActivateRoadmapProjection() .Subscribe(_ => ActivateLastCreatedProjection()) .AddToDispose(connectionDisposableList); + this.WhenAnyValue(m => m.LastUpdatedMode) + .Where(mode => mode) + .Take(1) + .Subscribe(_ => ActivateLastUpdatedProjection()) + .AddToDispose(connectionDisposableList); + this.WhenAnyValue(m => m.UnlockedMode) .Where(mode => mode) .Take(1) @@ -1086,7 +1175,7 @@ void ActivateLastOpenedProjection() public void SelectCurrentTask() { - if (AllTasksMode ^ UnlockedMode ^ CompletedMode ^ ArchivedMode ^ GraphMode ^ LastCreatedMode ^ LastOpenedMode) + if (AllTasksMode ^ UnlockedMode ^ CompletedMode ^ ArchivedMode ^ GraphMode ^ LastCreatedMode ^ LastUpdatedMode ^ LastOpenedMode) { if (AllTasksMode) { @@ -1124,6 +1213,11 @@ public void SelectCurrentTask() if (CurrentLastCreated?.TaskItem != CurrentTaskItem) CurrentLastCreated = FindTaskWrapperViewModel(CurrentTaskItem, LastCreatedItems); } + else if (LastUpdatedMode) + { + if (CurrentLastUpdated?.TaskItem != CurrentTaskItem) + CurrentLastUpdated = FindTaskWrapperViewModel(CurrentTaskItem, LastUpdatedItems); + } else if (LastOpenedMode) { if (CurrentLastOpenedItem?.TaskItem != CurrentTaskItem) @@ -1234,6 +1328,7 @@ private void ExpandParentNodesForTask(TaskItemViewModel taskItem) public bool GraphMode { get; set; } public bool SettingsMode { get; set; } public bool LastCreatedMode { get; set; } + public bool LastUpdatedMode { get; set; } public bool LastOpenedMode { get; set; } public INotificationManagerWrapper ManagerWrapper { get; } @@ -1257,6 +1352,9 @@ private void ExpandParentNodesForTask(TaskItemViewModel taskItem) private ReadOnlyObservableCollection _lastCreatedItems; public ReadOnlyObservableCollection LastCreatedItems { get; set; } + private ReadOnlyObservableCollection _lastUpdatedItems; + public ReadOnlyObservableCollection LastUpdatedItems { get; set; } + public ReadOnlyObservableCollection _lastOpenedItems; public ReadOnlyObservableCollection LastOpenedItems { get; set; } @@ -1271,6 +1369,7 @@ private void ExpandParentNodesForTask(TaskItemViewModel taskItem) public TaskWrapperViewModel CurrentCompletedItem { get; set; } = null!; public TaskWrapperViewModel CurrentArchivedItem { get; set; } = null!; public TaskWrapperViewModel CurrentLastCreated { get; set; } = null!; + public TaskWrapperViewModel CurrentLastUpdated { get; set; } = null!; public TaskWrapperViewModel CurrentGraphItem { get; set; } = null!; public TaskWrapperViewModel CurrentLastOpenedItem { get; set; } = null!; @@ -1324,6 +1423,7 @@ private void ExpandParentNodesForTask(TaskItemViewModel taskItem) public DateFilter CompletedDateFilter { get; set; } = new(); public DateFilter ArchivedDateFilter { get; set; } = new(); public DateFilter LastCreatedDateFilter { get; set; } = new(); + public DateFilter LastUpdatedDateFilter { get; set; } = new(); public static ReadOnlyObservableCollection DateFilterDefinitions { get; set; } = DateFilterDefinition.GetDefinitions(); public object TabItems { get; } = null!; diff --git a/src/Unlimotion.ViewModel/SortDefinition.cs b/src/Unlimotion.ViewModel/SortDefinition.cs index 352e665..0a401d5 100644 --- a/src/Unlimotion.ViewModel/SortDefinition.cs +++ b/src/Unlimotion.ViewModel/SortDefinition.cs @@ -26,6 +26,7 @@ public static IEnumerable GetDefinitions() new(w => w.TaskItem.ArchiveDateTime), new(w => w.TaskItem.UnlockedDateTime), new(w => w.TaskItem.CreatedDateTime), + new(w => w.TaskItem.UpdatedDateTime), } }; //Π­ΠΌΠΎΠ΄Π·ΠΈ @@ -55,6 +56,24 @@ public static IEnumerable GetDefinitions() new(w => w.TaskItem.CreatedDateTime, SortDirection.Descending) } }; + //По Π΄Π°Ρ‚Π΅ обновлСния Asc + yield return new SortDefinition + { + Name = "Updated Ascending", + Comparer = new SortExpressionComparer + { + new(w => w.TaskItem.UpdatedDateTime) + } + }; + //По Π΄Π°Ρ‚Π΅ обновлСния Des + yield return new SortDefinition + { + Name = "Updated Descending", + Comparer = new SortExpressionComparer + { + new(w => w.TaskItem.UpdatedDateTime, SortDirection.Descending) + } + }; //По Π΄Π°Ρ‚Π΅ Ρ€Π°Π·Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²ΠΊΠΈ Asc yield return new SortDefinition { diff --git a/src/Unlimotion.ViewModel/TaskItemViewModel.cs b/src/Unlimotion.ViewModel/TaskItemViewModel.cs index 184f174..ca6c66e 100644 --- a/src/Unlimotion.ViewModel/TaskItemViewModel.cs +++ b/src/Unlimotion.ViewModel/TaskItemViewModel.cs @@ -402,6 +402,7 @@ public TaskItem Model Title = Title, Description = Description, CreatedDateTime = CreatedDateTime, + UpdatedDateTime = UpdatedDateTime, UnlockedDateTime = UnlockedDateTime, CompletedDateTime = CompletedDateTime, ArchiveDateTime = ArchiveDateTime, @@ -434,6 +435,7 @@ public TaskItem Model public bool? IsCompleted { get; set; } public int Version { get; set; } 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; } @@ -616,6 +618,7 @@ public void Update(TaskItem taskItem) if (Title != taskItem.Title) Title = taskItem.Title; if (Description != taskItem.Description) Description = taskItem.Description; if (CreatedDateTime != taskItem.CreatedDateTime) CreatedDateTime = taskItem.CreatedDateTime; + if (UpdatedDateTime != taskItem.UpdatedDateTime) UpdatedDateTime = taskItem.UpdatedDateTime; if (UnlockedDateTime != taskItem.UnlockedDateTime) UnlockedDateTime = taskItem.UnlockedDateTime; if (CompletedDateTime != taskItem.CompletedDateTime) CompletedDateTime = taskItem.CompletedDateTime; if (ArchiveDateTime != taskItem.ArchiveDateTime) ArchiveDateTime = taskItem.ArchiveDateTime; diff --git a/src/Unlimotion/Views/MainControl.axaml b/src/Unlimotion/Views/MainControl.axaml index ce35960..6845bd5 100644 --- a/src/Unlimotion/Views/MainControl.axaml +++ b/src/Unlimotion/Views/MainControl.axaml @@ -151,7 +151,7 @@ Command="{Binding RemoveCommand}"/> SelectionMode="Single"> - + + + + + + + + + + + + + + + + + +