fix: replace broken add-filter button with stepped filter builder

- Fixed popover z-index: uses fixed positioning with z-50 above backdrop
- Stepped flow: select field → set operator (is/is_not) → choose/write value → Apply
- Removed old inline CF value inputs (handled inline in the new flow)
- Fixed filter persistence: clear filters when navigating away from saved view
- Fixed home redirect: check for default dashboard on load

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 11:30:17 +02:00
parent b70a133ea2
commit 7be90684fb

View File

@@ -159,6 +159,9 @@ function TicketWorkbenchContent() {
const [saveViewOpen, setSaveViewOpen] = useState(false);
const [saveViewName, setSaveViewName] = useState("");
const [addFilterOpen, setAddFilterOpen] = useState(false);
const [addFilterField, setAddFilterField] = useState<string | null>(null); // which field type is selected
const [addFilterOperator, setAddFilterOperator] = useState("is");
const [addFilterValue, setAddFilterValue] = useState("");
const [dialogOpen, setDialogOpen] = useState(false);
const [newSubject, setNewSubject] = useState("");
@@ -653,10 +656,18 @@ function TicketWorkbenchContent() {
</button>
</span>
))}
<div className="relative">
<div>
<button
type="button"
onClick={() => setAddFilterOpen((prev) => !prev)}
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
const popover = document.getElementById("add-filter-popover");
if (popover) {
popover.style.left = `${rect.left}px`;
popover.style.top = `${rect.bottom + 4}px`;
}
setAddFilterOpen((prev) => !prev);
}}
className="inline-flex h-7 items-center gap-1 rounded border border-dashed border-border px-2 text-xs font-medium text-muted-foreground hover:border-ring hover:text-foreground transition-colors"
>
<PlusIcon className="h-3 w-3" />
@@ -664,100 +675,179 @@ function TicketWorkbenchContent() {
</button>
{addFilterOpen && (
<>
<div className="fixed inset-0 z-10" onClick={() => setAddFilterOpen(false)} />
<div className="absolute left-0 top-full z-20 mt-1 w-52 rounded-md border border-border bg-card p-1 shadow-lg">
<div className="px-2 py-1.5 text-[10px] font-semibold uppercase text-muted-foreground">Queue</div>
{queues.map((q) => (
<button
key={`q:${q.id}`}
type="button"
className="w-full rounded px-2 py-1.5 text-left text-xs text-foreground hover:bg-accent"
onClick={() => {
if (!filters.find((f) => f.field === "queue")) {
setFilters((prev) => [...prev, {
id: crypto.randomUUID(),
field: "queue",
operator: "is",
value: q.id,
label: buildFilterLabel("queue", "is", q.name),
}]);
}
setAddFilterOpen(false);
}}
>
{q.name}
</button>
))}
<div className="mt-0.5 border-t border-border pt-0.5" />
<div className="px-2 py-1.5 text-[10px] font-semibold uppercase text-muted-foreground">Owner</div>
<button
type="button"
className="w-full rounded px-2 py-1.5 text-left text-xs text-foreground hover:bg-accent"
onClick={() => {
if (!filters.find((f) => f.field === "owner")) {
setFilters((prev) => [...prev, {
id: crypto.randomUUID(),
field: "owner",
operator: "is",
value: "unassigned",
label: buildFilterLabel("owner", "is", "Unassigned"),
}]);
}
setAddFilterOpen(false);
}}
>
Unassigned
</button>
{users.map((u) => (
<button
key={`o:${u.id}`}
type="button"
className="w-full rounded px-2 py-1.5 text-left text-xs text-foreground hover:bg-accent"
onClick={() => {
if (!filters.find((f) => f.field === "owner")) {
setFilters((prev) => [...prev, {
id: crypto.randomUUID(),
field: "owner",
operator: "is",
value: u.id,
label: buildFilterLabel("owner", "is", u.username),
}]);
}
setAddFilterOpen(false);
}}
>
{u.username}
</button>
))}
{customFields.length > 0 && (
<div
className="fixed inset-0 z-40"
onClick={() => {
setAddFilterOpen(false);
setAddFilterField(null);
}}
/>
<div
id="add-filter-popover"
className="fixed z-50 w-52 rounded-md border border-border bg-popover p-1 shadow-lg"
>
{!addFilterField ? (
/* Step 1: choose field */
<>
<div className="mt-0.5 border-t border-border pt-0.5" />
<div className="px-2 py-1.5 text-[10px] font-semibold uppercase text-muted-foreground">Custom field</div>
<button
type="button"
className="w-full rounded px-2 py-1.5 text-left text-xs text-foreground hover:bg-accent"
onClick={() => {
if (!filters.find((f) => f.field === "queue")) {
setAddFilterField("queue");
setAddFilterOperator("is");
setAddFilterValue("");
} else {
setAddFilterOpen(false);
}
}}
>
Queue
</button>
<button
type="button"
className="w-full rounded px-2 py-1.5 text-left text-xs text-foreground hover:bg-accent"
onClick={() => {
if (!filters.find((f) => f.field === "owner")) {
setAddFilterField("owner");
setAddFilterOperator("is");
setAddFilterValue("");
} else {
setAddFilterOpen(false);
}
}}
>
Owner
</button>
{customFields.map((cf) => (
<button
key={`cf:${cf.id}`}
key={`cf-field-${cf.id}`}
type="button"
className="w-full rounded px-2 py-1.5 text-left text-xs text-foreground hover:bg-accent"
onClick={() => {
const cfFilter: Filter = {
id: crypto.randomUUID(),
field: `cf.${cf.key}`,
operator: "is",
value: "",
label: `${cf.name} is ...`,
};
setFilters((prev) => [...prev, cfFilter]);
setAddFilterOpen(false);
setTimeout(() => {
const input = document.getElementById(`cf-value-${cfFilter.id}`) as HTMLInputElement;
input?.focus();
}, 50);
setAddFilterField(`cf.${cf.key}`);
setAddFilterOperator("is");
setAddFilterValue("");
}}
>
{cf.name}
</button>
))}
</>
) : (
/* Step 2: operator + value */
<div className="space-y-2 p-1">
<div className="flex items-center gap-1 text-xs">
<button
type="button"
onClick={() => {
setAddFilterField(null);
}}
className="text-muted-foreground hover:text-foreground"
>
</button>
<span className="font-medium text-foreground">
{addFilterField.startsWith("cf.") ? addFilterField.slice(3) : addFilterField}
</span>
</div>
<select
value={addFilterOperator}
onChange={(e) => setAddFilterOperator(e.target.value)}
className="h-7 w-full rounded border border-input bg-card px-2 text-xs outline-none"
>
<option value="is">is</option>
<option value="is_not">is not</option>
</select>
{addFilterField === "queue" ? (
<select
value={addFilterValue}
onChange={(e) => setAddFilterValue(e.target.value)}
className="h-7 w-full rounded border border-input bg-card px-2 text-xs outline-none"
>
<option value="">Select queue...</option>
{queues.map((q) => (
<option key={q.id} value={q.id}>{q.name}</option>
))}
</select>
) : addFilterField === "owner" ? (
<select
value={addFilterValue}
onChange={(e) => setAddFilterValue(e.target.value)}
className="h-7 w-full rounded border border-input bg-card px-2 text-xs outline-none"
>
<option value="">Select owner...</option>
<option value="unassigned">Unassigned</option>
{users.map((u) => (
<option key={u.id} value={u.id}>{u.username}</option>
))}
</select>
) : (
<input
value={addFilterValue}
onChange={(e) => setAddFilterValue(e.target.value)}
placeholder="Value"
className="h-7 w-full rounded border border-input bg-card px-2 text-xs outline-none"
onKeyDown={(e) => {
if (e.key === "Enter" && addFilterValue.trim()) {
const field = addFilterField;
const value = addFilterValue;
let valueLabel = value;
setFilters((prev) => [...prev, {
id: crypto.randomUUID(),
field,
operator: addFilterOperator,
value,
label: buildFilterLabel(field, addFilterOperator, valueLabel),
}]);
setAddFilterField(null);
setAddFilterOpen(false);
}
}}
/>
)}
<div className="flex items-center justify-end gap-1 pt-1">
<button
type="button"
onClick={() => {
setAddFilterField(null);
setAddFilterOpen(false);
}}
className="rounded px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
>
Cancel
</button>
<button
type="button"
disabled={!addFilterValue.trim()}
onClick={() => {
if (!addFilterValue.trim()) return;
const field = addFilterField;
const value = addFilterValue;
const operator = addFilterOperator;
let valueLabel = value;
if (field === "queue") {
valueLabel = queues.find((q) => q.id === value)?.name ?? value;
} else if (field === "owner") {
valueLabel = value === "unassigned" ? "Unassigned"
: users.find((u) => u.id === value)?.username ?? value;
}
setFilters((prev) => [...prev, {
id: crypto.randomUUID(),
field,
operator,
value,
label: buildFilterLabel(field, operator, valueLabel),
}]);
setAddFilterField(null);
setAddFilterOpen(false);
}}
className="rounded bg-primary px-2 py-1 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
>
Apply
</button>
</div>
</div>
)}
</div>
</>
@@ -765,72 +855,6 @@ function TicketWorkbenchContent() {
</div>
</div>
{/* Inline value inputs for custom field filters */}
{filters
.filter((f) => f.field.startsWith("cf.") && f.value === "")
.map((f) => {
const cf = customFields.find((x) => x.key === f.field.slice(3));
const options = Array.isArray(cf?.values)
? cf.values.map((v) => String(v))
: [];
return (
<div key={`cf-input-${f.id}`} className="flex items-center gap-1.5">
<span className="text-xs font-medium text-muted-foreground">{cf?.name ?? f.field.slice(3)}:</span>
{options.length > 0 ? (
<select
id={`cf-value-${f.id}`}
value=""
onChange={(event) => {
const val = event.target.value;
if (!val) return;
setFilters((prev) =>
prev.map((x) =>
x.id === f.id
? { ...x, value: val, label: buildFilterLabel(x.field, "is", val) }
: x
)
);
}}
className="h-7 rounded border border-input bg-card px-2 text-xs outline-none"
>
<option value="">Select value...</option>
{options.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
) : (
<div className="flex items-center gap-1">
<input
id={`cf-value-${f.id}`}
placeholder="value"
className="h-7 w-32 rounded border border-input bg-card px-2 text-xs outline-none focus:border-ring"
onKeyDown={(event) => {
if (event.key === "Enter") {
const val = event.currentTarget.value.trim();
if (val) {
setFilters((prev) =>
prev.map((x) =>
x.id === f.id
? { ...x, value: val, label: buildFilterLabel(x.field, "is", val) }
: x
)
);
}
}
}}
/>
<button
type="button"
onClick={() => setFilters((prev) => prev.filter((x) => x.id !== f.id))}
className="text-xs text-muted-foreground hover:text-foreground"
>
cancel
</button>
</div>
)}
</div>
);
})}
</div>
</div>
</header>