feat: initial scaffold of kanban_cpp v2 (issue 0130)
Frontend C++ ImGui (main.cpp + 4 paneles) + backend Go (HTTP + SQLite + fsnotify + SSE). Reusa parse/scan/watch funcs del registry (issue 0130a).
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
#include "panels.h"
|
||||
#include "data.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/icons_tabler.h"
|
||||
|
||||
namespace kanban {
|
||||
|
||||
static std::string join(const std::vector<std::string>& 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<std::string>& 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<std::string> split_csv(const std::string& s) {
|
||||
std::vector<std::string> 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<std::string>& 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<std::mutex> 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<const char*> 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<const char*> 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<const char*> 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<std::string>& 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
|
||||
Reference in New Issue
Block a user