Skip to content

Commit 5b4371f

Browse files
committed
feat(webapp): stable column toggle and a clearer smart-column icon
Toggling a column now hides/shows it in place instead of moving it to a separate section, so the list never reorders when you check a box (order lives in the URL, hidden columns keep their slot). Marks smart columns with a variable icon, and gives them an explicit remove action distinct from hiding.
1 parent efe0ffd commit 5b4371f

5 files changed

Lines changed: 181 additions & 149 deletions

File tree

apps/webapp/app/components/runs/v3/AddSmartColumnDialog.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CodeBracketIcon } from "@heroicons/react/20/solid";
1+
import { VariableIcon } from "@heroicons/react/20/solid";
22
import { useEffect, useMemo, useState } from "react";
33
import { useTypedFetcher } from "remix-typedjson";
44
import { Button } from "~/components/primitives/Buttons";
@@ -253,7 +253,7 @@ function SmartColumnResolvedPreview({
253253
return (
254254
<div className="rounded border border-grid-dimmed">
255255
<div className="flex items-center gap-1 border-b border-grid-dimmed px-2 py-1">
256-
<CodeBracketIcon className="size-3.5 flex-none text-text-dimmed" />
256+
<VariableIcon className="size-3.5 flex-none text-text-dimmed" />
257257
<span className="truncate text-xs text-text-bright">{label || "Column"}</span>
258258
</div>
259259
<div className="px-2 py-1.5 text-right text-sm tabular-nums text-text-bright">{value}</div>

apps/webapp/app/components/runs/v3/RunsDisplayOptions.tsx

Lines changed: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {
22
ArrowUturnLeftIcon,
3-
CodeBracketIcon,
43
PencilSquareIcon,
54
PlusIcon,
5+
VariableIcon,
66
ViewColumnsIcon,
7+
XMarkIcon,
78
} from "@heroicons/react/20/solid";
89
import { GripVerticalIcon } from "lucide-react";
910
import { useMemo, useState } from "react";
@@ -16,9 +17,9 @@ import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
1617
import { useSearchParams } from "~/hooks/useSearchParam";
1718
import { cn } from "~/utils/cn";
1819
import {
19-
availableStandardColumns,
2020
encodeColumnLayout,
2121
resolveColumnLayout,
22+
type LayoutColumn,
2223
type ResolvedColumn,
2324
type RunColumnRuntime,
2425
type SmartColumnDef,
@@ -54,54 +55,55 @@ export function RunsDisplayOptions() {
5455
[cols.join(" "), sc.join(" "), runtime.isManagedCloud, runtime.isDevelopment]
5556
);
5657

57-
const available = availableStandardColumns(runtime);
58-
const visibleStandardCount = layout.visible.filter((c) => c.kind === "standard").length;
58+
const totalCount = layout.ordered.filter((o) => o.col.kind === "standard").length;
59+
const shownCount = layout.ordered.filter((o) => o.col.kind === "standard" && !o.hidden).length;
5960

60-
const applyVisible = (nextVisible: ResolvedColumn[]) => {
61-
const encoded = encodeColumnLayout(nextVisible, runtime);
61+
const applyLayout = (next: LayoutColumn[]) => {
62+
const encoded = encodeColumnLayout(next, runtime);
6263
replace({
6364
cols: encoded.cols.length > 0 ? encoded.cols : undefined,
6465
sc: encoded.sc.length > 0 ? encoded.sc : undefined,
6566
});
6667
};
6768

68-
const hideStandard = (id: string) => {
69-
applyVisible(layout.visible.filter((c) => !(c.kind === "standard" && c.def.id === id)));
70-
};
71-
72-
const showStandard = (id: string) => {
73-
const def = available.find((c) => c.id === id);
74-
if (!def) return;
75-
applyVisible([...layout.visible, { kind: "standard", def }]);
69+
const toggleHidden = (key: string) => {
70+
applyLayout(
71+
layout.ordered.map((o) => (keyFor(o.col) === key ? { ...o, hidden: !o.hidden } : o))
72+
);
7673
};
7774

7875
const removeSmart = (index: number) => {
79-
applyVisible(layout.visible.filter((c) => !(c.kind === "smart" && c.index === index)));
76+
applyLayout(layout.ordered.filter((o) => !(o.col.kind === "smart" && o.col.index === index)));
8077
};
8178

8279
const submitSmart = (def: SmartColumnDef) => {
8380
if (editing) {
84-
applyVisible(
85-
layout.visible.map((c) =>
86-
c.kind === "smart" && c.index === editing.index ? { ...c, def } : c
81+
applyLayout(
82+
layout.ordered.map((o) =>
83+
o.col.kind === "smart" && o.col.index === editing.index
84+
? { ...o, col: { ...o.col, def } }
85+
: o
8786
)
8887
);
8988
} else {
90-
applyVisible([...layout.visible, { kind: "smart", index: layout.smartColumns.length, def }]);
89+
applyLayout([
90+
...layout.ordered,
91+
{ col: { kind: "smart", index: layout.smartColumns.length, def }, hidden: false },
92+
]);
9193
}
9294
};
9395

9496
const reset = () => replace({ cols: undefined, sc: undefined });
9597

9698
const reorder = (fromKey: string, toKey: string) => {
9799
if (fromKey === toKey) return;
98-
const arr = [...layout.visible];
99-
const from = arr.findIndex((c) => keyFor(c) === fromKey);
100-
const to = arr.findIndex((c) => keyFor(c) === toKey);
100+
const arr = [...layout.ordered];
101+
const from = arr.findIndex((o) => keyFor(o.col) === fromKey);
102+
const to = arr.findIndex((o) => keyFor(o.col) === toKey);
101103
if (from < 0 || to < 0) return;
102104
const [moved] = arr.splice(from, 1);
103105
arr.splice(to, 0, moved);
104-
applyVisible(arr);
106+
applyLayout(arr);
105107
};
106108

107109
const endDrag = () => {
@@ -121,49 +123,37 @@ export function RunsDisplayOptions() {
121123
<div className="flex items-center justify-between px-3 py-2">
122124
<span className="text-xs font-medium text-text-dimmed">Columns</span>
123125
<span className="text-xs text-text-dimmed">
124-
{visibleStandardCount} of {available.length}
126+
{shownCount} of {totalCount}
125127
</span>
126128
</div>
127129
<div className="max-h-80 overflow-y-auto border-y border-grid-dimmed">
128-
{layout.visible.map((col) => (
129-
<ColumnRow
130-
key={keyFor(col)}
131-
col={col}
132-
checked
133-
draggable
134-
locked={col.kind === "standard" && !!col.def.locked}
135-
dragging={dragKey === keyFor(col)}
136-
isOver={overKey === keyFor(col) && dragKey !== keyFor(col)}
137-
onDragStart={() => setDragKey(keyFor(col))}
138-
onDragEnter={() => setOverKey(keyFor(col))}
139-
onDragEnd={endDrag}
140-
onDrop={() => {
141-
if (dragKey) reorder(dragKey, keyFor(col));
142-
endDrag();
143-
}}
144-
onToggle={() => {
145-
if (col.kind === "smart") removeSmart(col.index);
146-
else if (!col.def.locked) hideStandard(col.def.id);
147-
}}
148-
onEdit={
149-
col.kind === "smart"
150-
? () => setEditing({ index: col.index, def: col.def })
151-
: undefined
152-
}
153-
/>
154-
))}
155-
{layout.hiddenStandard.map((def) => (
156-
<ColumnRow
157-
key={`std:${def.id}`}
158-
col={{ kind: "standard", def }}
159-
checked={false}
160-
draggable={false}
161-
locked={false}
162-
dragging={false}
163-
isOver={false}
164-
onToggle={() => showStandard(def.id)}
165-
/>
166-
))}
130+
{layout.ordered.map(({ col, hidden }) => {
131+
const key = keyFor(col);
132+
return (
133+
<ColumnRow
134+
key={key}
135+
col={col}
136+
checked={!hidden}
137+
locked={col.kind === "standard" && !!col.def.locked}
138+
dragging={dragKey === key}
139+
isOver={overKey === key && dragKey !== key}
140+
onDragStart={() => setDragKey(key)}
141+
onDragEnter={() => setOverKey(key)}
142+
onDragEnd={endDrag}
143+
onDrop={() => {
144+
if (dragKey) reorder(dragKey, key);
145+
endDrag();
146+
}}
147+
onToggle={() => toggleHidden(key)}
148+
onEdit={
149+
col.kind === "smart"
150+
? () => setEditing({ index: col.index, def: col.def })
151+
: undefined
152+
}
153+
onRemove={col.kind === "smart" ? () => removeSmart(col.index) : undefined}
154+
/>
155+
);
156+
})}
167157
</div>
168158
<div className="flex flex-col p-1">
169159
<button
@@ -205,29 +195,29 @@ export function RunsDisplayOptions() {
205195
function ColumnRow({
206196
col,
207197
checked,
208-
draggable,
209198
locked,
210199
dragging,
211200
isOver,
212201
onToggle,
213202
onEdit,
203+
onRemove,
214204
onDragStart,
215205
onDragEnter,
216206
onDragEnd,
217207
onDrop,
218208
}: {
219209
col: ResolvedColumn;
220210
checked: boolean;
221-
draggable: boolean;
222211
locked: boolean;
223212
dragging: boolean;
224213
isOver: boolean;
225214
onToggle: () => void;
226215
onEdit?: () => void;
227-
onDragStart?: () => void;
228-
onDragEnter?: () => void;
229-
onDragEnd?: () => void;
230-
onDrop?: () => void;
216+
onRemove?: () => void;
217+
onDragStart: () => void;
218+
onDragEnter: () => void;
219+
onDragEnd: () => void;
220+
onDrop: () => void;
231221
}) {
232222
const isSmart = col.kind === "smart";
233223

@@ -237,18 +227,16 @@ function ColumnRow({
237227
"relative flex h-8 items-center gap-2 px-3 transition-colors hover:bg-charcoal-750",
238228
dragging && "opacity-40"
239229
)}
240-
draggable={draggable}
230+
draggable
241231
onDragStart={onDragStart}
242232
onDragEnter={onDragEnter}
243233
onDragEnd={onDragEnd}
244-
onDragOver={(e) => {
245-
if (draggable) e.preventDefault();
246-
}}
234+
onDragOver={(e) => e.preventDefault()}
247235
onDrop={onDrop}
248236
>
249237
{isOver && <div className="absolute inset-x-0 top-0 h-0.5 bg-blue-500" />}
250238
{locked ? <Checkbox checked disabled /> : <Checkbox checked={checked} onChange={onToggle} />}
251-
{isSmart && <CodeBracketIcon className="size-4 flex-none text-text-dimmed" />}
239+
{isSmart && <VariableIcon className="size-4 flex-none text-text-dimmed" />}
252240
<span
253241
className={cn("flex-1 truncate text-sm", checked ? "text-text-bright" : "text-text-dimmed")}
254242
>
@@ -264,11 +252,17 @@ function ColumnRow({
264252
<PencilSquareIcon className="size-3.5" />
265253
</button>
266254
)}
267-
{draggable ? (
268-
<GripVerticalIcon className="size-4 cursor-grab text-text-dimmed active:cursor-grabbing" />
269-
) : (
270-
<div className="size-4" />
255+
{onRemove && (
256+
<button
257+
type="button"
258+
onClick={onRemove}
259+
aria-label={`Remove ${col.def.label}`}
260+
className="flex size-5 items-center justify-center rounded text-text-dimmed transition-colors hover:text-error focus-custom"
261+
>
262+
<XMarkIcon className="size-3.5" />
263+
</button>
271264
)}
265+
<GripVerticalIcon className="size-4 cursor-grab text-text-dimmed active:cursor-grabbing" />
272266
</div>
273267
);
274268
}

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
ArrowPathIcon,
33
ArrowRightIcon,
44
ClockIcon,
5-
CodeBracketIcon,
5+
VariableIcon,
66
CpuChipIcon,
77
NoSymbolIcon,
88
RectangleStackIcon,
@@ -482,7 +482,7 @@ function SmartColumnHeader({ def }: { def: SmartColumnDef }) {
482482
return (
483483
<TableHeaderCell>
484484
<span className="flex items-center gap-1">
485-
<CodeBracketIcon className="size-3.5 flex-none text-text-dimmed" />
485+
<VariableIcon className="size-3.5 flex-none text-text-dimmed" />
486486
<span className="truncate">{def.label}</span>
487487
</span>
488488
</TableHeaderCell>

0 commit comments

Comments
 (0)