╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │568 data: *const Self, │ │569 session: *ui.Session, │ │570 // per-split state, indexed like the stack's split children: the issue the │ │568 - // pane shows, and its description and author text boxes' focus ids. │ │571 + // pane shows, and its description, author and open/close button focus ids.│ │572 detailed_index: [stack_child_max]?usize, │ │573 description_id: [stack_child_max]?usize, │ │574 author_id: [stack_child_max]?usize, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │575 + status_button_id: [stack_child_max]?usize,│ │576 │ │577 const header_index: usize = 0; │ │578 const stack_index: usize = 1; │ ╰───────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │763 .detailed_index = @splat(null), │ │764 .description_id = @splat(null), │ │765 .author_id = @splat(null), │ │766 + .status_button_id = @splat(null),│ │767 }; │ │768 } │ │769 │ ╰──────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │1407 inner.children.clearAndFree(allocator); │ │1408 inner.getFocus().child_id = null; │ │1409 self.author_id[index] = null; │ │1410 + self.status_button_id[index] = null; │ │1411 │ │1412 // the tool row: the open/close and edit buttons; the description page│ │1413 // shows none. │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1428 try row.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1429 } │ │1430 │ │1431 + // a terminal has no file picker, so attaching is web only. the │ │1432 + // renderer covers this button with a file input, so clicking it │ │1433 + // opens the browser's own picker and posts what it chose. │ │1434 + if (!self.session.is_terminal and !entry.conflicted and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1435 + const label = "add attachment"; │ │1436 + var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1437 + errdefer button.deinit(allocator); │ │1438 + button.getFocus().focusable = true; │ │1439 + button.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1440 + try std.fmt.allocPrint(pa, "{s}/issue:{s}/attach", .{ ui.file_input_prefix, entry.id }) │ │1441 + else │ │1442 + try std.fmt.allocPrint(pa, "{s}/repo/{s}/issue:{s}/attach", .{ ui.file_input_prefix, self.data.identity, entry.id }) }; │ │1443 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } });│ │1444 + } │ │1445 + │ │1446 // a conflicted issue's only action is resolving it. the button │ │1447 // stays visible logged out; the resolve page shows the │ │1448 // unauthorized view then. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1456 try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } });│ │1457 } else { │ │1458 // a logged-out session can't flip the status, so it gets no │ │1438 - // open/close button (and no form action on the row) │ │1459 + // open/close button │ │1460 if (self.session.data.is_local or self.session.data.user_id != null) { │ │1461 const action: []const u8 = switch (entry.issue.event.status) { │ │1462 .open => "close", │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1463 .closed => "open", │ │1464 }; │ │1465 + // the renderer distinguishes plain clickables from buttons │ │1466 + // that should POST to a server route by the submit kind. │ │1467 row.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1468 try std.fmt.allocPrint(pa, "form:/issue:{s}/{s}", .{ entry.id, action })│ │1469 else │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1472 var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1473 errdefer button.deinit(allocator); │ │1474 button.getFocus().focusable = true; │ │1452 - // the renderer distinguishes plain clickables from buttons that │ │1453 - // should POST to a server route by this kind. │ │1475 + // the renderer distinguishes plain clickables from buttons │ │1476 + // that should POST to a server route by this kind. │ │1477 button.getFocus().kind = .{ .custom = "submit" }; │ │1478 + self.status_button_id[index] = button.getFocus().id; │ │1479 try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │1480 } │ │1481 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1490 } │ │1491 } │ │1492 │ │1469 - // the resolve button on a conflicted issue, else the open/close │ │1470 - // button when present, the edit button otherwise │ │1471 - row.getFocus().child_id = row.children.keys()[button_in_row_index];│ │1493 + // the leftmost button: the attachment button when the session has │ │1494 + // one, else the resolve button on a conflicted issue, the │ │1495 + // open/close button when present, the edit button otherwise │ │1496 + row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1497 } │ │1498 │ │1499 if (comment_page) |page| { │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1587 break :blk tb.getFocus().id; │ │1588 }; │ │1589 │ │1590 + try Attachment.appendRows(allocator, inner, self.session, self.data.identity, entry.attachments); │ │1591 + │ │1592 if (!description_page) { │ │1593 var reply = try Comment.linkBox(allocator, self.session, "new comment", ui.RoutablePage.repoCommentNewRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong);│ │1594 errdefer reply.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯