fix: match original table UI with immediate per-action submission
All checks were successful
Check / check (pull_request) Successful in 4s

Replace the Save All workflow with the original per-action behavior:
- Edit row: shows Save/Cancel buttons, submits full set immediately
- Delete row: shows confirmation dialog, submits full set immediately
- Add row: submits full set immediately on Add click

Moves Alpine.js logic into a proper envVarEditor component in
app-detail.js. Initializes env var data from hidden span elements
with data attributes for safe HTML escaping.

All actions collect the complete env var set and POST to the single
bulk endpoint POST /apps/{id}/env — no Save All button needed.
This commit is contained in:
clawbot
2026-03-10 11:23:36 -07:00
parent 690b7d4590
commit 3f96f4f81b
2 changed files with 113 additions and 83 deletions

View File

@@ -6,6 +6,68 @@
*/ */
document.addEventListener("alpine:init", () => { document.addEventListener("alpine:init", () => {
// ============================================
// Environment Variable Editor Component
// ============================================
Alpine.data("envVarEditor", () => ({
vars: [],
editIdx: -1,
editKey: "",
editVal: "",
init() {
this.vars = Array.from(this.$el.querySelectorAll(".env-init")).map(
(span) => ({
key: span.dataset.key,
value: span.dataset.value,
}),
);
},
startEdit(i) {
this.editIdx = i;
this.editKey = this.vars[i].key;
this.editVal = this.vars[i].value;
},
saveEdit(i) {
this.vars[i] = { key: this.editKey, value: this.editVal };
this.editIdx = -1;
this.submitAll();
},
removeVar(i) {
if (!window.confirm("Delete this environment variable?")) {
return;
}
this.vars.splice(i, 1);
this.submitAll();
},
addVar(keyEl, valEl) {
const k = keyEl.value.trim();
const v = valEl.value.trim();
if (!k) {
return;
}
this.vars.push({ key: k, value: v });
this.submitAll();
},
submitAll() {
this.$refs.bulkData.value = this.vars
.map((e) => e.key + "=" + e.value)
.join("\n");
this.$refs.bulkForm.submit();
},
}));
// ============================================
// App Detail Page Component
// ============================================
Alpine.data("appDetail", (config) => ({ Alpine.data("appDetail", (config) => ({
appId: config.appId, appId: config.appId,
currentDeploymentId: config.initialDeploymentId, currentDeploymentId: config.initialDeploymentId,

View File

@@ -101,91 +101,59 @@
</div> </div>
<!-- Environment Variables --> <!-- Environment Variables -->
<div class="card p-6 mb-6" x-data="{ <div class="card p-6 mb-6" x-data="envVarEditor()">
vars: [],
newKey: '',
newValue: '',
init() {
const text = this.$refs.envVarsField.value;
if (!text.trim()) return;
this.vars = text.split('\n')
.map(l => l.trim())
.filter(l => l !== '')
.map(l => {
const idx = l.indexOf('=');
if (idx === -1) return null;
return { key: l.substring(0, idx), value: l.substring(idx + 1), editing: false };
})
.filter(v => v !== null);
},
addVar() {
if (this.newKey.trim() === '') return;
this.vars.push({ key: this.newKey.trim(), value: this.newValue, editing: false });
this.newKey = '';
this.newValue = '';
},
removeVar(index) {
this.vars.splice(index, 1);
},
prepareSubmit() {
this.$refs.envVarsField.value = this.vars.map(v => v.key + '=' + v.value).join('\n');
}
}">
<h2 class="section-title mb-4">Environment Variables</h2> <h2 class="section-title mb-4">Environment Variables</h2>
<form method="POST" action="/apps/{{.App.ID}}/env" @submit="prepareSubmit()"> {{range .EnvVars}}<span class="env-init hidden" data-key="{{.Key}}" data-value="{{.Value}}"></span>{{end}}
<template x-if="vars.length > 0">
<div class="overflow-x-auto mb-4">
<table class="table">
<thead class="table-header">
<tr>
<th>Key</th>
<th>Value</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="table-body">
<template x-for="(env, idx) in vars" :key="idx">
<tr>
<template x-if="editIdx !== idx">
<td class="font-mono font-medium" x-text="env.key"></td>
</template>
<template x-if="editIdx !== idx">
<td class="font-mono text-gray-500" x-text="env.value"></td>
</template>
<template x-if="editIdx !== idx">
<td class="text-right">
<button @click="startEdit(idx)" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
<button @click="removeVar(idx)" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</td>
</template>
<template x-if="editIdx === idx">
<td colspan="3">
<form @submit.prevent="saveEdit(idx)" class="flex gap-2 items-center">
<input type="text" x-model="editKey" required class="input flex-1 font-mono text-sm">
<input type="text" x-model="editVal" required class="input flex-1 font-mono text-sm">
<button type="submit" class="btn-primary text-sm">Save</button>
<button type="button" @click="editIdx = -1" class="text-gray-500 hover:text-gray-700 text-sm">Cancel</button>
</form>
<p class="text-xs text-amber-600 mt-1">⚠ Container restart needed after env var changes.</p>
</td>
</template>
</tr>
</template>
</tbody>
</table>
</div>
</template>
<form @submit.prevent="addVar($refs.newKey, $refs.newVal)" class="flex flex-col sm:flex-row gap-2">
<input x-ref="newKey" type="text" placeholder="KEY" required class="input flex-1 font-mono text-sm">
<input x-ref="newVal" type="text" placeholder="value" required class="input flex-1 font-mono text-sm">
<button type="submit" class="btn-primary">Add</button>
</form>
<form x-ref="bulkForm" method="POST" action="/apps/{{.App.ID}}/env" class="hidden">
{{ .CSRFField }} {{ .CSRFField }}
<template x-if="vars.length > 0"> <textarea x-ref="bulkData" name="env_vars"></textarea>
<div class="overflow-x-auto mb-4">
<table class="table">
<thead class="table-header">
<tr>
<th>Key</th>
<th>Value</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody class="table-body">
<template x-for="(v, index) in vars" :key="index">
<tr>
<template x-if="!v.editing">
<td class="font-mono font-medium" x-text="v.key"></td>
</template>
<template x-if="!v.editing">
<td class="font-mono text-gray-500" x-text="v.value"></td>
</template>
<template x-if="!v.editing">
<td class="text-right">
<button type="button" @click="v.editing = true" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</button>
<button type="button" @click="removeVar(index)" class="text-error-500 hover:text-error-700 text-sm">Delete</button>
</td>
</template>
<template x-if="v.editing">
<td colspan="3">
<div class="flex gap-2 items-center">
<input type="text" x-model="v.key" required class="input flex-1 font-mono text-sm">
<input type="text" x-model="v.value" required class="input flex-1 font-mono text-sm">
<button type="button" @click="v.editing = false" class="btn-primary text-sm">Done</button>
</div>
<p class="text-xs text-amber-600 mt-1">⚠ Container restart needed after env var changes.</p>
</td>
</template>
</tr>
</template>
</tbody>
</table>
</div>
</template>
<div class="flex flex-col sm:flex-row gap-2 mb-3">
<input type="text" x-model="newKey" placeholder="KEY" class="input flex-1 font-mono text-sm">
<input type="text" x-model="newValue" placeholder="value" class="input flex-1 font-mono text-sm">
<button type="button" @click="addVar()" class="btn-secondary">Add</button>
</div>
<textarea name="env_vars" x-ref="envVarsField" class="hidden">{{range .EnvVars}}{{.Key}}={{.Value}}
{{end}}</textarea>
<div class="flex items-center gap-3">
<button type="submit" class="btn-primary">Save All</button>
<p class="text-xs text-amber-600">⚠ Container restart needed after env var changes.</p>
</div>
</form> </form>
</div> </div>