╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────╮ │14 const inp = @import("../input.zig"); │ │15 const diff3 = @import("../../diff3.zig"); │ │16 const Comment = @import("Comment.zig"); │ │17 +const Attachment = @import("Attachment.zig"); │ │18 │ │19 const wasm = builtin.target.cpu.arch == .wasm32;│ │20 │ ╰────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────╮ │38 // whether the issue has an unresolved merge conflict │ │39 conflicted: bool = false, │ │40 comments: Comment.Window = .empty, │ │41 + attachments: []const Attachment.WithId = &.{}, │ │42 }; │ │43 │ │44 // one side of a conflicted field: its value and who set it│ ╰───────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │445 &id_hex, │ │446 if (std.mem.eql(u8, &id_hex, selected_id)) comments_start else 0, │ │447 ), │ │448 + .attachments = try Attachment.load(hash_kind, arena, haxy_moment, &id_hex),│ │449 }); │ │450 } │ │451 │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯