flat kvps in step detail modals
3clyp50 committed
Jan 29, 2026 at 12:54 UTC
c25815f752da02771f2675863a30c4b331abdc74
2 files changed
+67
-56
webui/components/modals/process-step-detail/process-step-detail.html
+18
-23
@@ -60,12 +60,16 @@
60
</div>
61
</template>
62
63
- <!-- Generic KVPs for all types -->
63
+ <!-- Flattened KVPs -->
64
<template x-if="$store.stepDetail.selectedStepForDetail?.kvps">
65
- <div class="content-block">
66
- <h4>Details</h4>
67
- <div class="kvps-display"><template x-for="[key, value] in Object.entries($store.stepDetail.selectedStepForDetail?.kvps || {})" :key="key"><span class="kvp-line"><span class="kvp-key" x-text="$store.stepDetail.formatKey(key)"></span><span class="kvp-value" x-text="$store.stepDetail.formatValue(value)"></span></span></template></div>
68
- <div class="step-action-buttons" data-segment="details"></div>
65
+ <div class="kvp-boxes">
66
+ <template x-for="[key, value] in Object.entries($store.stepDetail.flattenKvps($store.stepDetail.selectedStepForDetail?.kvps) || {})" :key="key">
67
+ <div class="content-block kvp-box">
68
+ <h4 x-text="key"></h4>
69
+ <pre class="kvp-value-content" x-text="$store.stepDetail.formatFlatValue(value)"></pre>
70
+ <div class="step-action-buttons kvp-box-actions" x-init="$store.stepDetail.renderKvpCopyButton($el, key, value)"></div>
71
+ </div>
72
+ </template>
73
</div>
74
</template>
75
@@ -225,31 +229,22 @@
229
word-break: break-word;
230
}
231
228
- .kvps-display {
232
+ .kvp-boxes {
233
display: flex;
234
flex-direction: column;
231
- gap: 0.25rem;
232
- font-size: 0.9rem;
233
- line-height: 1.5;
234
- }
235
-
236
- .kvp-line {
237
- display: block;
238
- }
239
-
240
- .kvp-key {
241
- color: var(--color-text-muted);
242
- margin-right: 0.5rem;
243
- }
244
-
245
- .kvp-key::after {
246
- content: ':';
235
+ gap: 1rem;
236
}
237
249
- .kvp-value {
238
+ .kvp-value-content {
239
color: var(--color-text);
240
white-space: pre-wrap;
241
word-break: break-word;
242
+ margin: 0;
243
+ font-size: 0.85rem;
244
+ }
245
+
246
+ .kvp-box-actions {
247
+ margin-top: 0.5rem;
248
}
249
250
.step-action-buttons {
webui/components/modals/process-step-detail/step-detail-store.js
+49
-33
@@ -22,22 +22,21 @@ const model = {
22
renderSegmentButtons() {
23
const step = this.selectedStepForDetail;
24
25
- // heading
26
- document.querySelector('[data-segment="heading"]')?.replaceChildren(
27
- createActionButton("copy", "", () => copyToClipboard(this.cleanHeading(step.heading)))
28
- );
29
-
30
- // details (kvps)
31
- const kvpsText = Object.entries(step.kvps || {})
32
- .map(([k, v]) => `${this.formatKey(k)}: ${this.formatValue(v)}`)
33
- .join('\n');
34
- document.querySelector('[data-segment="details"]')?.replaceChildren(
35
- createActionButton("copy", "", () => copyToClipboard(kvpsText))
36
- );
25
+ [
26
+ { segment: "heading", value: this.cleanHeading(step?.heading) },
27
+ { segment: "content", value: step?.content ?? "" }
28
+ ].forEach(({ segment, value }) => {
29
+ document.querySelector(`[data-segment="${segment}"]`)?.replaceChildren(
30
+ createActionButton("copy", "", () => copyToClipboard(value))
31
+ );
32
+ });
33
+ },
34
38
- // content
39
- document.querySelector('[data-segment="content"]')?.replaceChildren(
40
- createActionButton("copy", "", () => copyToClipboard(step.content))
35
+ // render copy buttons for individual kvp boxes
36
+ renderKvpCopyButton(container, key, value) {
37
+ const copyText = this.formatFlatValue(value);
38
+ container?.replaceChildren(
39
+ createActionButton("copy", "", () => copyToClipboard(copyText))
40
);
41
},
42
@@ -59,7 +58,8 @@ const model = {
58
if (!step) return "";
59
const lines = [];
60
lines.push(`Type: ${step.type || "unknown"}`);
62
- if (step.heading) lines.push(`Heading: ${step.heading}`);
61
+ const heading = this.cleanHeading(step.heading);
62
+ if (heading) lines.push(`Heading: ${heading}`);
63
if (step.timestamp) {
64
const date = new Date(parseFloat(step.timestamp) * 1000);
65
lines.push(`Timestamp: ${date.toISOString()}`);
@@ -68,9 +68,9 @@ const model = {
68
if (step.kvps) {
69
lines.push("");
70
lines.push("--- Data ---");
71
- for (const [key, value] of Object.entries(step.kvps)) {
72
- const formattedValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
73
- lines.push(`${key}: ${formattedValue}`);
71
+ const flattenedKvps = this.flattenKvps(step.kvps);
72
+ for (const [key, value] of Object.entries(flattenedKvps)) {
73
+ lines.push(this.buildKvpCopyText(key, value));
74
}
75
}
76
if (step.content) {
@@ -183,9 +183,9 @@ const model = {
183
184
// Format value for display (handles objects)
185
formatValue(value) {
186
- if (value === null || value === undefined) return '';
187
- if (typeof value === 'object') return JSON.stringify(value, null, 2);
188
- return String(value);
186
+ return value == null
187
+ ? ''
188
+ : (typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value));
189
},
190
191
// Format key for display (title case)
@@ -193,17 +193,33 @@ const model = {
193
return key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
194
},
195
196
- // Clean text value (handle arrays, remove brackets)
197
- cleanTextValue(value) {
198
- if (Array.isArray(value)) {
199
- return value
200
- .filter(item => item && String(item).trim() && !/^[\[\]]$/.test(String(item).trim()))
201
- .join('\n');
202
- }
203
- if (typeof value === 'object' && value !== null) {
204
- return JSON.stringify(value, null, 2);
205
- }
206
- return String(value).replace(/^\s*[\[\]]\s*$/gm, '').trim();
196
+ // flatten nested kvps into top-level entries
197
+ flattenKvps(kvps, parentKey = "") {
198
+ const isPlainObject = value => value && typeof value === "object" && !Array.isArray(value);
199
+ return Object.entries(kvps || {}).reduce((acc, [key, value]) => {
200
+ const fullKey = [parentKey, this.formatKey(key)].filter(Boolean).join(" / ");
201
+ return {
202
+ ...acc,
203
+ ...(isPlainObject(value) ? this.flattenKvps(value, fullKey) : { [fullKey]: value })
204
+ };
205
+ }, {});
206
+ },
207
+
208
+ // format values
209
+ formatFlatValue(value) {
210
+ return Array.isArray(value)
211
+ ? value
212
+ .map(item => this.formatValue(item))
213
+ .filter(item => item.trim())
214
+ .join('\n')
215
+ : this.formatValue(value);
216
+ },
217
+
218
+ // build copy text
219
+ buildKvpCopyText(key, value) {
220
+ const formattedValue = this.formatFlatValue(value);
221
+ const separator = formattedValue.includes("\n") ? ":\n" : ": ";
222
+ return `${key}${separator}${formattedValue}`;
223
},
224
225
// Clean heading by removing icon:// prefixes