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
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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user