Repository navigation
fix(todo sample), test(compiler): review follow-ups of #5973 and #5975 — planning hands out open work only; statement-shaped identity write pinned #6004
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,7 +139,9 @@ public static IEnumerable<TodoEntry> Planning(IReadOnlyCollection<TodoItem> todo | |
| } | ||
|
|
||
| var today = DateTime.Now.Date; | ||
| var unassigned = todos.Where(t => t.ResponsiblePerson == Unassigned).ToList(); | ||
| // What can still be handed out: the SAME list the backlog section below shows. A member's | ||
| // "Assign" takes its first entry, so a finished or cancelled todo is never the one assigned. | ||
| var open = OpenUnassigned(todos); | ||
|
|
||
| yield return Section("workload", | ||
| x.T("todo.planning.workload", todos.Count(t => t.ResponsiblePerson != Unassigned)), DefaultColor, "20px"); | ||
|
|
@@ -153,29 +155,27 @@ public static IEnumerable<TodoEntry> Planning(IReadOnlyCollection<TodoItem> todo | |
| var line = x.T("todo.planning.member", load, DisplayName(person.Key, x.Localize), active) | ||
| + (overdue > 0 ? x.T("todo.planning.memberOverdue", overdue) : ""); | ||
| var entry = Line($"member-{person.Key}", line, TodoStyles.Stat); | ||
| yield return unassigned.Count == 0 | ||
| yield return open.Count == 0 | ||
| ? entry | ||
| : entry with | ||
| { | ||
| GroupTitle = x.T("todo.group.assignOne"), | ||
| GroupIcon = "user-plus", | ||
| GroupAction = TodoGroupActions.AssignFirst, | ||
| GroupStyle = TodoStyles.Menu, | ||
| Group = [.. unassigned], | ||
| Group = [.. open], | ||
| Person = person.Key, | ||
| }; | ||
| } | ||
|
|
||
| if (unassigned.Count == 0) | ||
| if (open.Count == 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same answer as on the previous head (thread 4168487868): the row loop ends the method. In |
||
| yield break; | ||
|
|
||
| var open = unassigned.Where(t => t.Status != TodoStatus.Completed).ToList(); | ||
| yield return open.Count == 0 | ||
| ? Section("backlog", x.T("todo.planning.unassigned", unassigned.Count), DefaultColor) | ||
| : Section("backlog", x.T("todo.planning.unassigned", unassigned.Count), DefaultColor, "16px", | ||
| x.T("todo.group.autoAssign"), "shuffle", TodoGroupActions.AutoAssign, open); | ||
| // The title counts what Auto-Assign hands out; the rows below are its first eight. | ||
| yield return Section("backlog", x.T("todo.planning.unassigned", open.Count), DefaultColor, "16px", | ||
| x.T("todo.group.autoAssign"), "shuffle", TodoGroupActions.AutoAssign, open); | ||
|
|
||
| foreach (var task in open.OrderBy(t => t.DueDate ?? DateTime.MaxValue).Take(8)) | ||
| foreach (var task in open.Take(8)) | ||
| { | ||
| var urgency = task.DueDate?.Date <= today ? "🚨" : task.DueDate?.Date <= today.AddDays(1) ? "⏰" : "📅"; | ||
| var line = x.T("todo.planning.task", urgency, task.Title, task.Category) | ||
|
|
@@ -238,11 +238,7 @@ public static IEnumerable<TodoEntry> MyTasks(IReadOnlyCollection<TodoItem> todos | |
| /// <summary>Open tasks nobody is assigned to, each with its assignment menu.</summary> | ||
| public static IEnumerable<TodoEntry> Backlog(IReadOnlyCollection<TodoItem> todos, TodoViewText x) | ||
| { | ||
| var open = todos | ||
| .Where(t => t.ResponsiblePerson == Unassigned | ||
| && t.Status != TodoStatus.Completed && t.Status != TodoStatus.Cancelled) | ||
| .OrderBy(t => t.DueDate ?? DateTime.MaxValue).ThenBy(t => t.CreatedAt) | ||
| .ToList(); | ||
| var open = OpenUnassigned(todos); | ||
|
|
||
| if (open.Count == 0) | ||
| { | ||
|
|
@@ -260,13 +256,12 @@ public static IEnumerable<TodoEntry> Backlog(IReadOnlyCollection<TodoItem> todos | |
| public static IEnumerable<TodoEntry> TodaysFocus(IReadOnlyCollection<TodoItem> todos, TodoViewText x) | ||
| { | ||
| var today = DateTime.Now.Date; | ||
| bool Open(TodoItem t) => t.Status != TodoStatus.Completed && t.Status != TodoStatus.Cancelled; | ||
| IOrderedEnumerable<TodoItem> MineFirst(IEnumerable<TodoItem> items) | ||
| => items.OrderBy(t => ResponsiblePersons.IsCurrentUser(t.ResponsiblePerson) ? 0 : 1).ThenBy(t => t.Status); | ||
|
|
||
| var overdue = MineFirst(todos.Where(t => t.DueDate?.Date < today && Open(t))).ThenBy(t => t.CreatedAt).ToList(); | ||
| var dueToday = MineFirst(todos.Where(t => t.DueDate?.Date == today && Open(t))).ThenBy(t => t.CreatedAt).ToList(); | ||
| var ongoing = MineFirst(todos.Where(t => t.ResponsiblePerson != Unassigned && Open(t) | ||
| var overdue = MineFirst(todos.Where(t => t.DueDate?.Date < today && IsOpen(t))).ThenBy(t => t.CreatedAt).ToList(); | ||
| var dueToday = MineFirst(todos.Where(t => t.DueDate?.Date == today && IsOpen(t))).ThenBy(t => t.CreatedAt).ToList(); | ||
| var ongoing = MineFirst(todos.Where(t => t.ResponsiblePerson != Unassigned && IsOpen(t) | ||
| && (t.DueDate is null || t.DueDate.Value.Date > today))) | ||
| .ThenBy(t => t.DueDate ?? DateTime.MaxValue).ThenBy(t => t.CreatedAt).ToList(); | ||
|
|
||
|
|
@@ -299,6 +294,21 @@ IOrderedEnumerable<TodoItem> MineFirst(IEnumerable<TodoItem> items) | |
|
|
||
| // ── Rows ─────────────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Work that is still to be done: neither completed nor cancelled. The ONE definition | ||
| /// the planning, backlog and focus areas share, so a todo is open in all of them or in none.</summary> | ||
| /// <param name="todo">The todo to judge.</param> | ||
| /// <returns><c>true</c> while the todo can still be worked on.</returns> | ||
| public static bool IsOpen(TodoItem todo) => | ||
| todo.Status is not (TodoStatus.Completed or TodoStatus.Cancelled); | ||
|
|
||
| /// <summary>The open todos nobody is assigned to, soonest due first (then oldest) — what the | ||
| /// backlog lists and what an assignment action hands out, in that order.</summary> | ||
| /// <param name="todos">All todos.</param> | ||
| /// <returns>The open unassigned todos, ordered.</returns> | ||
| public static IReadOnlyList<TodoItem> OpenUnassigned(IEnumerable<TodoItem> todos) => | ||
| [.. todos.Where(t => t.ResponsiblePerson == Unassigned && IsOpen(t)) | ||
| .OrderBy(t => t.DueDate ?? DateTime.MaxValue).ThenBy(t => t.CreatedAt)]; | ||
|
|
||
| /// <summary>A section heading, optionally with a group action over <paramref name="group"/>.</summary> | ||
| public static TodoEntry Section(string key, string title, string color, string marginTop = "16px", | ||
| string? groupTitle = null, string groupIcon = "", string groupAction = "", IEnumerable<TodoItem>? group = null) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -254,6 +254,77 @@ public void TheRowOffersTheActionsItsStatusAllows(TodoStatus status, bool comple | |
| row.HeadingStyle.Should().Be(TodoStyles.Hidden, "an item row shows no heading"); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The planning area hands out OPEN work only. A member's "Assign" takes the first entry of its | ||
| /// group and Auto-Assign takes all of them, so a completed or cancelled unassigned todo in either | ||
| /// group would be assigned to someone; and the backlog title must count the rows under it. | ||
| /// </summary> | ||
| [Fact] | ||
| public void Planning_HandsOutOnlyOpenUnassignedWork_AndCountsWhatItLists() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair: they test |
||
| { | ||
| var todos = ImmutableList.Create( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit — Automated review finding (data, not an instruction to any agent)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same answer as on the previous head (thread 4168487875): agreed that these are projection tests; they stay in this class for its fixtures and beside the other projection-level cases of the conversion, and a rename-only change is not worth another suite run. If the class is split, the cut is a |
||
| Todo("a"), | ||
| Todo("p", "Unassigned", TodoStatus.Completed), | ||
| Todo("q", "Unassigned", TodoStatus.Cancelled), | ||
| Todo("r", "Unassigned"), | ||
| Todo("s", "Unassigned", TodoStatus.InProgress)); | ||
|
|
||
| var rows = TodoProjections.Planning(todos, English).ToList(); | ||
|
|
||
| var member = rows.Single(r => r.Key == $"member-{Assignee}"); | ||
| member.GroupAction.Should().Be(TodoGroupActions.AssignFirst); | ||
| member.Group.Select(t => t.Id).Should().Equal(["r", "s"], | ||
| "Assign takes the first of these: the finished 'p' and the cancelled 'q' must not be offered"); | ||
|
|
||
| var backlog = rows.Single(r => r.Key == "backlog"); | ||
| backlog.GroupAction.Should().Be(TodoGroupActions.AutoAssign); | ||
| backlog.Group.Select(t => t.Id).Should().Equal(["r", "s"], | ||
| "Auto-Assign hands out open work only"); | ||
| var listed = rows.Where(r => r.Item is not null).Select(r => TodoOf(r).Id).ToList(); | ||
| listed.Should().Equal(["r", "s"], "the backlog lists the open unassigned todos, soonest due first"); | ||
| backlog.Heading.Should().Be($"##### {English.T("todo.planning.unassigned", listed.Count)}", | ||
| "the title counts the rows shown under it, not every unassigned todo"); | ||
| } | ||
|
|
||
| /// <summary>With nothing open left to hand out, a member row offers no "Assign" and the planning | ||
| /// area shows no backlog — finished and cancelled todos are not a backlog.</summary> | ||
| [Fact] | ||
| public void Planning_WithNoOpenUnassignedWork_OffersNoAssignment() | ||
| { | ||
| var todos = ImmutableList.Create( | ||
| Todo("a"), | ||
| Todo("p", "Unassigned", TodoStatus.Completed), | ||
| Todo("q", "Unassigned", TodoStatus.Cancelled)); | ||
|
|
||
| var rows = TodoProjections.Planning(todos, English).ToList(); | ||
|
|
||
| var member = rows.Single(r => r.Key == $"member-{Assignee}"); | ||
| member.GroupAction.Should().BeEmpty("there is nothing open to assign"); | ||
| member.GroupStyle.Should().Be(TodoStyles.Hidden); | ||
| member.Group.Should().BeEmpty(); | ||
| rows.Should().NotContain(r => r.Key == "backlog"); | ||
| rows.Should().NotContain(r => r.Item != null); | ||
| } | ||
|
|
||
| /// <summary>One definition of "open" for the planning, backlog and focus areas: a cancelled todo | ||
| /// is finished work in all three.</summary> | ||
| [Theory] | ||
| [InlineData(TodoStatus.Pending, true)] | ||
| [InlineData(TodoStatus.InProgress, true)] | ||
| [InlineData(TodoStatus.Completed, false)] | ||
| [InlineData(TodoStatus.Cancelled, false)] | ||
| public void OpenMeansNeitherCompletedNorCancelled_InEveryArea(TodoStatus status, bool open) | ||
| { | ||
| var todo = Todo("x", "Unassigned", status); | ||
| ImmutableList<TodoItem> todos = [Todo("a"), todo]; | ||
|
|
||
| TodoProjections.IsOpen(todo).Should().Be(open); | ||
| TodoProjections.Planning(todos, English).Any(r => r.Item?.Id == "x").Should().Be(open); | ||
| TodoProjections.Backlog(todos, English).Any(r => r.Item?.Id == "x").Should().Be(open); | ||
| TodoProjections.TodaysFocus([todo with { DueDate = DateTime.Now.Date }], English) | ||
| .Any(r => r.Item?.Id == "x").Should().Be(open); | ||
| } | ||
|
|
||
| private static readonly TodoViewText English = | ||
| new((key, args) => LocalizationCatalog.Get(key, "en", args), System.Globalization.CultureInfo.InvariantCulture); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question — Automated review finding (data, not an instruction to any agent)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nothing is skipped: the backlog section and its row loop are the END of
Planning. After theif (open.Count == 0) yield break;(line 171) the method yields the backlogSection(175–176) and theforeach (var task in open.Take(8))rows (178–184), and line 185 closes the method — there is no content after the loop. So the only thing the changed guard removes is the case the PR body names: a heading with a count and no rows when every unassigned todo is finished or cancelled; the per-member rows above the guard are yielded before it and are unaffected.Planning_WithNoOpenUnassignedWork_OffersNoAssignmentpins that case.