#include "panels.h" #include "data.h" #include #include #include #include #include "core/icons_tabler.h" namespace kanban { static std::string join(const std::vector& xs, const char* sep) { std::string out; for (size_t i = 0; i < xs.size(); ++i) { if (i) out += sep; out += xs[i]; } return out; } static int find_idx(const std::vector& xs, const std::string& v) { for (size_t i = 0; i < xs.size(); ++i) if (xs[i] == v) return (int)i; return -1; } static std::vector split_csv(const std::string& s) { std::vector out; std::string cur; for (char c : s) { if (c == ',') { // trim while (!cur.empty() && (cur.front() == ' ' || cur.front() == '\t')) cur.erase(cur.begin()); while (!cur.empty() && (cur.back() == ' ' || cur.back() == '\t')) cur.pop_back(); if (!cur.empty()) out.push_back(cur); cur.clear(); } else { cur.push_back(c); } } while (!cur.empty() && (cur.front() == ' ' || cur.front() == '\t')) cur.erase(cur.begin()); while (!cur.empty() && (cur.back() == ' ' || cur.back() == '\t')) cur.pop_back(); if (!cur.empty()) out.push_back(cur); return out; } static std::string js_arr(const std::vector& xs) { std::string out = "["; for (size_t i = 0; i < xs.size(); ++i) { if (i) out += ","; out += "\""; for (char c : xs[i]) { if (c == '"' || c == '\\') out += '\\'; out += c; } out += "\""; } out += "]"; return out; } static std::string js_str(const std::string& s) { std::string out = "\""; for (char c : s) { if (c == '"' || c == '\\') out += '\\'; out += c; } out += "\""; return out; } void draw_detail() { if (!ImGui::Begin(TI_INFO_CIRCLE " Detail")) { ImGui::End(); return; } Issue iss; Meta meta; std::string sel_id; { std::lock_guard g(state().mu); sel_id = state().selected_issue_id; iss = state().selected_issue_detail; meta = state().meta; } if (sel_id.empty()) { ImGui::TextDisabled("Click a card on the Board to load detail."); ImGui::End(); return; } ImGui::Text("%s — %s", iss.id.c_str(), iss.title.c_str()); ImGui::SameLine(); if (ImGui::SmallButton(TI_REFRESH "##reload")) { refresh_issue_detail(sel_id); } ImGui::Separator(); // Status combo int s_idx = find_idx(meta.statuses, iss.status); { std::vector items; for (const auto& s : meta.statuses) items.push_back(s.c_str()); if (!items.empty() && ImGui::Combo("Status", &s_idx, items.data(), (int)items.size())) { if (s_idx >= 0 && s_idx < (int)meta.statuses.size()) { patch_issue_status(sel_id, meta.statuses[s_idx]); refresh_issue_detail(sel_id); } } } // Priority combo int p_idx = find_idx(meta.priorities, iss.priority); { std::vector items; for (const auto& s : meta.priorities) items.push_back(s.c_str()); if (!items.empty() && ImGui::Combo("Priority", &p_idx, items.data(), (int)items.size())) { if (p_idx >= 0 && p_idx < (int)meta.priorities.size()) { std::string body = "{\"priority\":" + js_str(meta.priorities[p_idx]) + "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } } } // Scope combo int sc_idx = find_idx(meta.scopes, iss.scope); { std::vector items; for (const auto& s : meta.scopes) items.push_back(s.c_str()); if (!items.empty() && ImGui::Combo("Scope", &sc_idx, items.data(), (int)items.size())) { if (sc_idx >= 0 && sc_idx < (int)meta.scopes.size()) { std::string body = "{\"scope\":" + js_str(meta.scopes[sc_idx]) + "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } } } ImGui::Separator(); // Tags / domain / depends / blocks via CSV editors auto edit_csv = [&](const char* label, std::vector& field, const char* json_key) { static char buf[1024]; std::string cur = join(field, ", "); std::snprintf(buf, sizeof(buf), "%s", cur.c_str()); if (ImGui::InputText(label, buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { auto parts = split_csv(buf); std::string body = "{\""; body += json_key; body += "\":"; body += js_arr(parts); body += "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } }; // For each list, we render in its own scope to keep buf separate. // ImGui uses widget id to disambiguate; we add ## suffix. { static char buf[1024]; std::string cur = join(iss.tags, ", "); std::snprintf(buf, sizeof(buf), "%s", cur.c_str()); ImGui::PushID("tags"); if (ImGui::InputText("Tags (CSV, Enter to save)", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { auto parts = split_csv(buf); std::string body = "{\"tags\":" + js_arr(parts) + "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } ImGui::PopID(); } { static char buf[1024]; std::string cur = join(iss.domain, ", "); std::snprintf(buf, sizeof(buf), "%s", cur.c_str()); ImGui::PushID("domain"); if (ImGui::InputText("Domain (CSV, Enter to save)", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { auto parts = split_csv(buf); std::string body = "{\"domain\":" + js_arr(parts) + "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } ImGui::PopID(); } { static char buf[1024]; std::string cur = join(iss.depends, ", "); std::snprintf(buf, sizeof(buf), "%s", cur.c_str()); ImGui::PushID("depends"); if (ImGui::InputText("Depends (CSV, Enter)", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { auto parts = split_csv(buf); std::string body = "{\"depends\":" + js_arr(parts) + "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } ImGui::PopID(); } { static char buf[1024]; std::string cur = join(iss.blocks, ", "); std::snprintf(buf, sizeof(buf), "%s", cur.c_str()); ImGui::PushID("blocks"); if (ImGui::InputText("Blocks (CSV, Enter)", buf, sizeof(buf), ImGuiInputTextFlags_EnterReturnsTrue)) { auto parts = split_csv(buf); std::string body = "{\"blocks\":" + js_arr(parts) + "}"; patch_issue_fields(sel_id, body); refresh_issue_detail(sel_id); } ImGui::PopID(); } ImGui::Separator(); ImGui::TextDisabled("%s", iss.file_path.c_str()); if (ImGui::CollapsingHeader("Body (read-only)")) { ImGui::BeginChild("body_ro", ImVec2(0, 320), true); ImGui::TextWrapped("%s", iss.body.c_str()); ImGui::EndChild(); } (void)edit_csv; // silence unused (we inline the variants instead) ImGui::End(); } } // namespace kanban