feat(timesheet): add searchable tag selectors

This commit is contained in:
2026-04-25 19:09:10 +03:30
parent 8b16344aef
commit cf7dd06046
2 changed files with 77 additions and 25 deletions

View File

@@ -531,9 +531,10 @@ function TagMultiSelect({
title: string;
compact?: boolean;
portalOwnerId?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const wrapperRef = useRef<HTMLDivElement>(null);
}) {
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const wrapperRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [dropdownStyle, setDropdownStyle] = useState<React.CSSProperties>({});
@@ -585,11 +586,21 @@ function TagMultiSelect({
window.removeEventListener("resize", closeOnViewportChange);
window.removeEventListener("scroll", closeOnViewportChange, true);
};
}, [isOpen]);
const selectedLabels = tags.filter((tag) => selectedTags.includes(tag.id)).map((tag) => tag.name);
const joinedSelectedLabels = selectedLabels.join(" | ");
const buttonLabel = compact
}, [isOpen]);
useEffect(() => {
if (!isOpen) {
setSearchQuery("");
}
}, [isOpen]);
const selectedLabels = tags.filter((tag) => selectedTags.includes(tag.id)).map((tag) => tag.name);
const joinedSelectedLabels = selectedLabels.join(" | ");
const normalizedSearch = searchQuery.trim().toLowerCase();
const filteredTags = normalizedSearch
? tags.filter((tag) => tag.name.toLowerCase().includes(normalizedSearch))
: tags;
const buttonLabel = compact
? selectedTags.length > 0
? joinedSelectedLabels
: ""
@@ -629,19 +640,32 @@ function TagMultiSelect({
{isOpen && (
createPortal(
<div
ref={dropdownRef}
style={dropdownStyle}
data-entry-editor-owner={portalOwnerId}
className="rounded-2xl border border-slate-200 bg-white p-2 shadow-xl dark:border-slate-700 dark:bg-slate-800"
>
{tags.length === 0 ? (
<p className="px-2 py-2 text-sm text-slate-500 dark:text-slate-400">{emptyHint}</p>
) : (
<div className="max-h-56 space-y-1 overflow-y-auto">
{tags.map((tag) => {
const selected = selectedTags.includes(tag.id);
return (
<div
ref={dropdownRef}
style={dropdownStyle}
data-entry-editor-owner={portalOwnerId}
className="rounded-2xl border border-slate-200 bg-white p-2 shadow-xl dark:border-slate-700 dark:bg-slate-800"
>
{tags.length === 0 ? (
<p className="px-2 py-2 text-sm text-slate-500 dark:text-slate-400">{emptyHint}</p>
) : (
<>
<div className="border-b border-slate-200 p-2 dark:border-slate-700">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-400" />
<input
type="text"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Search tags..."
className="h-8 w-full rounded-md border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 outline-none transition focus:border-sky-400 focus:bg-white focus:ring-2 focus:ring-sky-500/20 dark:border-slate-700 dark:bg-slate-900 dark:text-white dark:focus:border-sky-500 dark:focus:bg-slate-800"
/>
</div>
</div>
<div className="max-h-72 space-y-1 overflow-y-auto p-2">
{filteredTags.map((tag) => {
const selected = selectedTags.includes(tag.id);
return (
<button
key={tag.id}
type="button"
@@ -2306,8 +2330,8 @@ export default function Timesheet() {
client: t.projects?.clientLabel || "Client",
tags: t.tags?.title || "Tags",
clear: extendedTimesheet.clearFilters || "Clear filters",
customFrom: extendedTimesheet.customFromLabel || "From",
customTo: extendedTimesheet.customToLabel || "To",
customFrom: extendedTimesheet.customFromLabel || "From date",
customTo: extendedTimesheet.customToLabel || "To date",
allClients: extendedTimesheet.allClientsLabel || "All clients",
allProjects: extendedTimesheet.allProjectsLabel || "All projects",
allTags: extendedTimesheet.allTagsLabel || "All tags",