╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Threads.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │1337 else => {}, │ │1338 }; │ │1339 │ │1369 - // on an edit page, register the issue's content as the inputs' │ │1340 + // on an edit page, register the thread's content as the inputs'│ │1341 // page-constant initial values, which the web overlay renders │ │1342 // into them. │ │1343 if (self.data.view == .edit) { │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1369 } │ │1370 │ │1371 try self.box.build(allocator, constraint, root_focus); │ │1401 - } │ │1402 - │ │1403 - fn refreshDetail(self: *This, allocator: std.mem.Allocator, index: usize) !void { │ │1404 - const sel = self.selectedThreadIndex(index) orelse return; │ │1405 - if (self.detailed_index[index]) |d| if (d == sel) return; │ │1406 - try self.populateDetail(allocator, index, sel); │ │1407 - self.detailed_index[index] = sel; │ │1372 } │ │1373 │ │1374 fn populateDetail(self: *This, allocator: std.mem.Allocator, index: usize, sel: usize) !void {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1375 const entry = self.window(index).items[sel]; │ │1376 const inner = self.detailInner(index); │ │1413 - // the /description page replaces its issue's detail with a back link │ │1414 - // and the whole description; other issues keep their normal detail. │ │1377 + // the /description page replaces its thread's detail with a back link │ │1378 + // and the whole description; other threads keep their normal detail. │ │1379 const description_page = self.data.description_page and std.mem.eql(u8, entry.id, self.data.selected_id); │ │1380 const comment_page = if (std.mem.eql(u8, entry.id, self.data.selected_id)) self.data.comment_page else null;│ │1381 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │1383 inner.children.clearAndFree(allocator); │ │1384 inner.getFocus().child_id = null; │ │1385 self.author_id[index] = null; │ │1422 - self.primary_button_id[index] = null; │ │1386 │ │1387 // the tool row; the description page shows none.│ │1388 { │ ╰───────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1406 if (entry.revision_ready) { │ │1407 const route = listRoute(self.data.identity, @enumFromInt(0), "", entry.id) orelse return error.RouteTooLong; │ │1408 row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/post", .{try route.toUrl(self.session.page_arena)}) }; │ │1446 - var button = try wgt.TextBox(ui.Widget).init(allocator, "post", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1447 - errdefer button.deinit(allocator); │ │1448 - button.getFocus().focusable = true; │ │1449 - button.getFocus().kind = .{ .custom = "submit" }; │ │1450 - self.primary_button_id[index] = button.getFocus().id; │ │1451 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "post".len + 2, .height = null } });│ │1409 + try addToolButton(allocator, row, "post", "submit"); │ │1410 } │ │1411 if (row.children.count() > first_in_row_index) │ │1412 row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1416 // opens the browser's own picker and posts what it chose. │ │1417 if (!self.session.is_terminal and !entryConflicted(entry) and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1418 const label = "add attachment"; │ │1461 - var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1462 - errdefer button.deinit(allocator); │ │1463 - button.getFocus().focusable = true; │ │1464 - button.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1419 + const action = if (self.data.identity.len == 0) │ │1420 try std.fmt.allocPrint(pa, "{s}/{s}:{s}/attach", .{ ui.file_input_prefix, @tagName(kind), entry.id }) │ │1421 else │ │1467 - try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }) }; │ │1468 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } });│ │1422 + try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }); │ │1423 + try addToolButton(allocator, row, label, action); │ │1424 } │ │1425 │ │1471 - // a conflicted issue's only action is resolving it. the button │ │1426 + // a conflicted thread's only action is resolving it. the button │ │1427 // stays visible logged out; the resolve page shows the │ │1428 // unauthorized view then. │ │1429 if (has_conflicts and entryConflicted(entry)) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1430 const label = "resolve conflict"; │ │1476 - var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1477 - errdefer button.deinit(allocator); │ │1478 - button.getFocus().focusable = true; │ │1431 const route = resolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1480 - button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1481 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } });│ │1432 + try addToolButton(allocator, row, label, try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │1433 } else { │ │1434 // a logged-out session can't flip the status, so it gets no │ │1435 // open/close button │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1436 if (has_status and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1486 - if (statusAction(entryStatus(entry))) |action| { │ │1487 - const route = commentsRoute(self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1488 - row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), action }) }; │ │1489 - │ │1490 - var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1491 - errdefer button.deinit(allocator); │ │1492 - button.getFocus().focusable = true; │ │1493 - // the renderer distinguishes plain clickables from buttons │ │1494 - // that should POST to a server route by this kind. │ │1495 - button.getFocus().kind = .{ .custom = "submit" }; │ │1496 - self.primary_button_id[index] = button.getFocus().id; │ │1497 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │1437 + if (statusChange(entryStatus(entry))) |change| { │ │1438 + const route = ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1439 + row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), change.action }) }; │ │1440 + try addToolButton(allocator, row, change.action, "submit"); │ │1441 } │ │1442 } │ │1443 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1501 - // the edit button links to the issue's edit page. │ │1444 + // the edit button links to the thread's edit page. │ │1445 { │ │1503 - var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1504 - errdefer button.deinit(allocator); │ │1505 - button.getFocus().focusable = true; │ │1506 - const route = editRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1507 - button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1508 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } });│ │1446 + const route = ui.RoutablePage.repoThreadEditRoute(kind, self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1447 + try addToolButton(allocator, row, "edit", try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │1448 } │ │1449 } │ │1450 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1451 if (self.session.data.is_local or self.session.data.user_id != null) { │ │1513 - var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1514 - errdefer remove.deinit(allocator); │ │1515 - remove.getFocus().focusable = true; │ │1516 - const route = removeRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1517 - remove.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1518 - try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } });│ │1452 + const route = ui.RoutablePage.repoThreadRemoveRoute(kind, self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1453 + try addToolButton(allocator, row, "✕", try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │1454 } │ │1455 │ │1456 // the leftmost button: the attachment button when the session has │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯