Add dropdown with toggle functionality to OwnerPicker
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 6s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 7s

Introduced a dropdown feature to the OwnerPicker component, allowing users to search or browse owners more effectively. The dropdown can be toggled open/closed and includes improved styling for better user experience. Added click-outside functionality to automatically close the dropdown when users interact elsewhere.
This commit is contained in:
2026-02-12 15:36:09 +01:00
parent 648ff07651
commit 505b93be4f
2 changed files with 84 additions and 26 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
import { apiFetch } from "../api";
import { useAuth } from "../state";
@@ -34,13 +34,25 @@ function toggleOwner(ids, userId) {
}
function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }) {
const pickerRef = useRef(null);
const [open, setOpen] = useState(false);
const filtered = candidates.filter((item) =>
item.email.toLowerCase().includes(query.trim().toLowerCase())
);
const selected = candidates.filter((item) => selectedIds.includes(item.user_id));
useEffect(() => {
const onPointerDown = (event) => {
if (!pickerRef.current?.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", onPointerDown);
return () => document.removeEventListener("mousedown", onPointerDown);
}, []);
return (
<div className="owner-picker">
<div className="owner-picker" ref={pickerRef}>
<div className="owner-selected">
{selected.length > 0 ? (
selected.map((item) => (
@@ -59,30 +71,41 @@ function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }
<span className="muted">No owners selected yet.</span>
)}
</div>
<input
type="text"
className="owner-search-input"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
placeholder="Search users by email..."
/>
<div className="owner-search-results">
{filtered.map((item) => {
const active = selectedIds.includes(item.user_id);
return (
<button
key={item.user_id}
type="button"
className={`owner-result ${active ? "active" : ""}`}
onClick={() => onToggle(item.user_id)}
>
<span>{item.email}</span>
<small>{item.role}</small>
</button>
);
})}
{filtered.length === 0 && <div className="owner-result-empty">No matching users.</div>}
<div className={`owner-search-shell ${open ? "open" : ""}`}>
<input
type="text"
className="owner-search-input"
value={query}
onChange={(e) => onQueryChange(e.target.value)}
onFocus={() => setOpen(true)}
onClick={() => setOpen(true)}
placeholder="Search users by email..."
/>
<button type="button" className="owner-search-toggle" onClick={() => setOpen((prev) => !prev)}>
v
</button>
</div>
{open && (
<div className="owner-dropdown">
<div className="owner-search-results">
{filtered.map((item) => {
const active = selectedIds.includes(item.user_id);
return (
<button
key={item.user_id}
type="button"
className={`owner-result ${active ? "active" : ""}`}
onClick={() => onToggle(item.user_id)}
>
<span>{item.email}</span>
<small>{item.role}</small>
</button>
);
})}
{filtered.length === 0 && <div className="owner-result-empty">No matching users.</div>}
</div>
</div>
)}
</div>
);
}

View File

@@ -752,6 +752,7 @@ button {
.owner-picker {
display: grid;
gap: 8px;
position: relative;
}
.owner-selected {
@@ -774,14 +775,48 @@ button {
font-weight: 600;
}
.owner-search-shell {
display: grid;
grid-template-columns: 1fr auto;
gap: 0;
}
.owner-search-shell.open .owner-search-input {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.owner-search-input {
width: 100%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.owner-search-toggle {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
min-width: 38px;
font-weight: 700;
padding: 0 10px;
}
.owner-dropdown {
position: absolute;
left: 0;
right: 0;
top: calc(100% - 2px);
z-index: 10;
border: 1px solid #3c73ac;
border-radius: 0 0 10px 10px;
background: #0f2549;
box-shadow: 0 12px 26px #03112777;
padding: 8px;
}
.owner-search-results {
display: grid;
gap: 6px;
max-height: 150px;
max-height: 180px;
overflow: auto;
padding-right: 2px;
}