diff --git a/assets/setup-ui/app.js b/assets/setup-ui/app.js index ed3ca78..2940383 100644 --- a/assets/setup-ui/app.js +++ b/assets/setup-ui/app.js @@ -754,7 +754,17 @@ ''; app.innerHTML = html; + // Widen the centered container when the form contains a table (kind: + // List) — 7 columns of inputs need more horizontal room than the + // default 620px. Removed when navigating to a non-table form. + var hasTable = questions.some(function (q) { + return q.kind === "List" && q.list_columns && q.list_columns.length > 0; + }); + document.body.classList.toggle("has-wide-form", !!hasTable); restoreFormValues(questions); + if (typeof setupTableQuestions === "function") { + setupTableQuestions(questions); + } setupVisibility(questions); app.querySelectorAll("#form-area input, #form-area select, #form-area textarea").forEach(function (el) { var handler = function () { diff --git a/assets/setup-ui/style.css b/assets/setup-ui/style.css index 4ff408c..42fb098 100644 --- a/assets/setup-ui/style.css +++ b/assets/setup-ui/style.css @@ -25,6 +25,12 @@ html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} body{font-family:var(--font);background:var(--bg);color:var(--fg);min-height:100dvh;display:flex;justify-content:center;padding:3rem 1rem} .container{width:100%;max-width:620px} +/* Wider container when the form contains a table (kind: List) so 5–7 + columns of inputs aren't squeezed. JS toggles this class on `` + after rendering a question that includes `list_columns`. */ +body.has-wide-form .container{max-width:1180px} +.table-wrap{overflow-x:auto} +.row-table{min-width:max-content} /* ── Animation ── */ .fade-in{animation:fadeIn .25s ease} diff --git a/src/bin/greentic_setup.rs b/src/bin/greentic_setup.rs index c45c22d..bd3c70a 100644 --- a/src/bin/greentic_setup.rs +++ b/src/bin/greentic_setup.rs @@ -322,6 +322,12 @@ fn run_ui_mode(cli: &Cli, i18n: &CliI18n) -> Result<()> { let bundle_dir = resolve_bundle_source(&bundle_path, i18n)?; bundle::validate_bundle_exists(&bundle_dir).context(i18n.t("cli.error.invalid_bundle"))?; + // Compute the write-back target up front so that after the UI session + // completes the extracted bundle dir gets re-packed (or copied) back + // to the user's original input — matches the behaviour of + // run_simple_setup which calls gtbundle::create_gtbundle directly. + let output_target = setup_output_target(&bundle_path)?; + // Load answers from --answers file for UI pre-fill (values + scope). let (prefill_answers, answers_tenant, answers_team, answers_env) = if let Some(answers_path) = &cli.answers @@ -375,6 +381,7 @@ fn run_ui_mode(cli: &Cli, i18n: &CliI18n) -> Result<()> { cli.locale.as_deref(), prefill_answers, scope_from_answers, + output_target, )) } diff --git a/src/cli_helpers/bundle.rs b/src/cli_helpers/bundle.rs index b611a16..1616cae 100644 --- a/src/cli_helpers/bundle.rs +++ b/src/cli_helpers/bundle.rs @@ -80,6 +80,7 @@ pub fn resolve_bundle_source(path: &std::path::Path, i18n: &CliI18n) -> Result

, shutdown_tx: broadcast::Sender<()>, #[allow(dead_code)] result: Mutex>, @@ -159,6 +165,7 @@ pub async fn launch( locale: Option<&str>, prefill_answers: Option>, scope_from_answers: bool, + output_target: Option, ) -> Result<()> { let (shutdown_tx, _) = broadcast::channel::<()>(1); @@ -171,6 +178,7 @@ pub async fn launch( locale: locale.map(String::from), prefill_answers, scope_from_answers, + output_target, shutdown_tx: shutdown_tx.clone(), result: Mutex::new(None), }); @@ -645,7 +653,8 @@ async fn post_execute( let _ = crate::platform_setup::persist_tunnel_artifact(&state.bundle_path, &tunnel); } - let result = tokio::task::spawn_blocking(move || { + let bundle_path_for_repack = bundle_path.clone(); + let mut result = tokio::task::spawn_blocking(move || { execute_setup(&bundle_path, &tenant, team.as_deref(), &env, answers) }) .await @@ -656,6 +665,68 @@ async fn post_execute( manual_steps: vec![], }); + // After a successful UI setup, re-pack the extracted bundle dir back + // to its original `.gtbundle` archive (or copy it to a directory + // output) so the on-disk artifact reflects the answers the user just + // saved. Without this the simple-mode CLI did the write-back but the + // UI mode silently dropped it — see bin/greentic_setup.rs:run_ui_mode. + if result.success + && let Some(target) = state.output_target.clone() + { + let repack = tokio::task::spawn_blocking(move || -> Result { + use crate::cli_helpers::{SetupOutputTarget, copy_dir_recursive}; + use crate::gtbundle; + match target { + SetupOutputTarget::Archive(out) => { + gtbundle::create_gtbundle(&bundle_path_for_repack, &out).with_context( + || { + format!( + "failed to write configured .gtbundle archive to {}", + out.display() + ) + }, + )?; + Ok(format!("Configured bundle written to: {}", out.display())) + } + SetupOutputTarget::Directory(out) => { + if out.exists() { + if out.is_dir() { + std::fs::remove_dir_all(&out).with_context(|| { + format!( + "failed to replace existing bundle directory {}", + out.display() + ) + })?; + } else { + std::fs::remove_file(&out).with_context(|| { + format!("failed to replace existing bundle file {}", out.display()) + })?; + } + } + copy_dir_recursive(&bundle_path_for_repack, &out, false) + .context("failed to write configured local bundle directory")?; + Ok(format!("Configured bundle written to: {}", out.display())) + } + } + }) + .await; + match repack { + Ok(Ok(msg)) => result.stdout.push_str(&format!("\n{msg}\n")), + Ok(Err(e)) => { + result.success = false; + result + .stderr + .push_str(&format!("\nWrite-back failed: {e:#}\n")); + } + Err(e) => { + result.success = false; + result + .stderr + .push_str(&format!("\nWrite-back panicked: {e}\n")); + } + } + } + *state.result.lock().unwrap() = Some(result.clone()); Json(result) }