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 { Link } from "react-router-dom";
|
||||||
import { apiFetch } from "../api";
|
import { apiFetch } from "../api";
|
||||||
import { useAuth } from "../state";
|
import { useAuth } from "../state";
|
||||||
@@ -34,13 +34,25 @@ function toggleOwner(ids, userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }) {
|
function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }) {
|
||||||
|
const pickerRef = useRef(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
const filtered = candidates.filter((item) =>
|
const filtered = candidates.filter((item) =>
|
||||||
item.email.toLowerCase().includes(query.trim().toLowerCase())
|
item.email.toLowerCase().includes(query.trim().toLowerCase())
|
||||||
);
|
);
|
||||||
const selected = candidates.filter((item) => selectedIds.includes(item.user_id));
|
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 (
|
return (
|
||||||
<div className="owner-picker">
|
<div className="owner-picker" ref={pickerRef}>
|
||||||
<div className="owner-selected">
|
<div className="owner-selected">
|
||||||
{selected.length > 0 ? (
|
{selected.length > 0 ? (
|
||||||
selected.map((item) => (
|
selected.map((item) => (
|
||||||
@@ -59,30 +71,41 @@ function OwnerPicker({ candidates, selectedIds, onToggle, query, onQueryChange }
|
|||||||
<span className="muted">No owners selected yet.</span>
|
<span className="muted">No owners selected yet.</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div className={`owner-search-shell ${open ? "open" : ""}`}>
|
||||||
type="text"
|
<input
|
||||||
className="owner-search-input"
|
type="text"
|
||||||
value={query}
|
className="owner-search-input"
|
||||||
onChange={(e) => onQueryChange(e.target.value)}
|
value={query}
|
||||||
placeholder="Search users by email..."
|
onChange={(e) => onQueryChange(e.target.value)}
|
||||||
/>
|
onFocus={() => setOpen(true)}
|
||||||
<div className="owner-search-results">
|
onClick={() => setOpen(true)}
|
||||||
{filtered.map((item) => {
|
placeholder="Search users by email..."
|
||||||
const active = selectedIds.includes(item.user_id);
|
/>
|
||||||
return (
|
<button type="button" className="owner-search-toggle" onClick={() => setOpen((prev) => !prev)}>
|
||||||
<button
|
v
|
||||||
key={item.user_id}
|
</button>
|
||||||
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>
|
||||||
|
{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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -752,6 +752,7 @@ button {
|
|||||||
.owner-picker {
|
.owner-picker {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.owner-selected {
|
.owner-selected {
|
||||||
@@ -774,14 +775,48 @@ button {
|
|||||||
font-weight: 600;
|
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 {
|
.owner-search-input {
|
||||||
width: 100%;
|
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 {
|
.owner-search-results {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
max-height: 150px;
|
max-height: 180px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user