Bläddra i källkod

RHL-037 feat(search): add clear all branches functionality to SearchMultiBranchPicker and SearchPage

Code_Uwe 3 veckor sedan
förälder
incheckning
1aceb8c02f

+ 2 - 0
components/search/SearchForm.jsx

@@ -25,6 +25,7 @@ export default function SearchForm({
 	branchesStatus,
 	selectedBranches,
 	onToggleBranch,
+	onClearAllBranches,
 	limit,
 	onLimitChange,
 }) {
@@ -83,6 +84,7 @@ export default function SearchForm({
 					availableBranches={availableBranches}
 					selectedBranches={selectedBranches}
 					onToggleBranch={onToggleBranch}
+					onClearAllBranches={onClearAllBranches}
 					isSubmitting={isSubmitting}
 				/>
 			) : null}

+ 13 - 0
components/search/SearchPage.jsx

@@ -157,6 +157,18 @@ export default function SearchPage({ branch: routeBranch }) {
 		[isAdminDev, urlState, replaceStateToUrl]
 	);
 
+	const handleClearAllBranches = React.useCallback(() => {
+		if (!isAdminDev) return;
+
+		const nextState = {
+			...urlState,
+			scope: SEARCH_SCOPE.MULTI,
+			branches: [],
+		};
+
+		replaceStateToUrl(nextState);
+	}, [isAdminDev, urlState, replaceStateToUrl]);
+
 	const handleLimitChange = React.useCallback(
 		(nextLimit) => {
 			const nextState = {
@@ -264,6 +276,7 @@ export default function SearchPage({ branch: routeBranch }) {
 					branchesStatus={branchesQuery.status}
 					selectedBranches={urlState.branches}
 					onToggleBranch={handleToggleBranch}
+					onClearAllBranches={handleClearAllBranches}
 					limit={urlState.limit}
 					onLimitChange={handleLimitChange}
 				/>

+ 137 - 155
components/search/form/SearchMultiBranchPicker.jsx

@@ -1,29 +1,15 @@
 "use client";
 
 import React from "react";
-import { Check, ChevronsUpDown, X } from "lucide-react";
+import { X } from "lucide-react";
 
 import { isValidBranchParam } from "@/lib/frontend/params";
-import { cn } from "@/lib/utils";
 
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Skeleton } from "@/components/ui/skeleton";
-import {
-	Popover,
-	PopoverContent,
-	PopoverTrigger,
-} from "@/components/ui/popover";
-import {
-	Command,
-	CommandEmpty,
-	CommandGroup,
-	CommandInput,
-	CommandItem,
-	CommandList,
-} from "@/components/ui/command";
 
 function normalizeTypedBranch(value) {
 	if (typeof value !== "string") return null;
@@ -36,11 +22,9 @@ export default function SearchMultiBranchPicker({
 	availableBranches,
 	selectedBranches,
 	onToggleBranch,
+	onClearAllBranches,
 	isSubmitting,
 }) {
-	const [open, setOpen] = React.useState(false);
-	const [manual, setManual] = React.useState("");
-
 	const listReady = branchesStatus === "ready";
 	const listLoading = branchesStatus === "loading";
 	const listError = branchesStatus === "error";
@@ -55,154 +39,152 @@ export default function SearchMultiBranchPicker({
 		[selected]
 	);
 
+	const canClearAll =
+		typeof onClearAllBranches === "function" &&
+		selected.length > 0 &&
+		!isSubmitting;
+
+	// Fail-open manual add (only relevant if branch list failed)
+	const [manual, setManual] = React.useState("");
 	const typed = normalizeTypedBranch(manual);
-	const canAddTyped = Boolean(
-		typed && isValidBranchParam(typed) && !selectedSet.has(typed)
-	);
+
+	const canAddTyped =
+		Boolean(typed && isValidBranchParam(typed)) && !selectedSet.has(typed);
 
 	return (
 		<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
 			<div className="space-y-3">
-				<p className="text-sm font-medium">Niederlassungen auswählen</p>
-				<p className="text-xs text-muted-foreground">
-					Wählen Sie eine oder mehrere Niederlassungen. Die Suche wird nach
-					jeder Änderung aktualisiert.
-				</p>
-
-				<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
-					<div className="flex flex-col gap-2">
-						<Label>Niederlassung hinzufügen</Label>
-
-						{listLoading ? (
-							<Skeleton className="h-9 w-full sm:w-[18rem]" />
-						) : (
-							<Popover open={open} onOpenChange={setOpen}>
-								<PopoverTrigger asChild>
-									<Button
-										type="button"
-										variant="outline"
-										role="combobox"
-										aria-expanded={open}
-										disabled={isSubmitting || !listReady}
-										className="w-full justify-between sm:w-[18rem]"
-										title={
-											listReady
-												? "Niederlassung hinzufügen"
-												: "Niederlassungen konnten nicht geladen werden"
-										}
-									>
-										Niederlassung hinzufügen…
-										<ChevronsUpDown className="h-4 w-4 opacity-70" />
-									</Button>
-								</PopoverTrigger>
-
-								<PopoverContent align="start" className="w-[18rem] p-0">
-									<Command>
-										<CommandInput placeholder="Suchen…" />
-										<CommandList>
-											<CommandEmpty>Keine Treffer.</CommandEmpty>
-											<CommandGroup>
-												{branchOptions.map((b) => {
-													const id = String(b);
-													const isSelected = selectedSet.has(id);
-
-													return (
-														<CommandItem
-															key={id}
-															value={id}
-															onSelect={(value) => {
-																const next = normalizeTypedBranch(value);
-																if (!next || !isValidBranchParam(next)) return;
-
-																if (!selectedSet.has(next))
-																	onToggleBranch(next);
-																setOpen(false);
-															}}
-														>
-															<Check
-																className={cn(
-																	"mr-2 h-4 w-4",
-																	isSelected ? "opacity-100" : "opacity-0"
-																)}
-															/>
-															{id}
-														</CommandItem>
-													);
-												})}
-											</CommandGroup>
-										</CommandList>
-									</Command>
-								</PopoverContent>
-							</Popover>
-						)}
-
-						{listError ? (
-							<div className="space-y-2">
+				<div className="flex items-start justify-between gap-4">
+					<div className="space-y-1">
+						<p className="text-sm font-medium">Niederlassungen auswählen</p>
+						<p className="text-xs text-muted-foreground">
+							Wählen Sie eine oder mehrere Niederlassungen. Die Suche wird nach
+							jeder Änderung aktualisiert.
+						</p>
+					</div>
+
+					<Button
+						type="button"
+						variant="outline"
+						size="sm"
+						disabled={!canClearAll}
+						onClick={() => {
+							if (!canClearAll) return;
+							onClearAllBranches();
+						}}
+						title="Alle Niederlassungen abwählen"
+					>
+						Alle abwählen
+					</Button>
+				</div>
+
+				{listLoading ? (
+					<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5">
+						{Array.from({ length: 15 }).map((_, i) => (
+							<div key={i} className="flex items-center gap-2">
+								<Skeleton className="h-4 w-4" />
+								<Skeleton className="h-4 w-14" />
+							</div>
+						))}
+					</div>
+				) : null}
+
+				{listError ? (
+					<div className="space-y-3">
+						<p className="text-xs text-muted-foreground">
+							Niederlassungen konnten nicht geladen werden. Sie können NLxx
+							manuell hinzufügen.
+						</p>
+
+						<div className="flex gap-2">
+							<Input
+								value={manual}
+								onChange={(e) => setManual(e.target.value)}
+								onKeyDown={(e) => {
+									if (e.key !== "Enter") return;
+									if (!canAddTyped) return;
+
+									e.preventDefault();
+									onToggleBranch(typed);
+									setManual("");
+								}}
+								placeholder="z. B. NL17"
+								disabled={isSubmitting}
+							/>
+							<Button
+								type="button"
+								variant="outline"
+								disabled={!canAddTyped || isSubmitting}
+								onClick={() => {
+									if (!canAddTyped) return;
+									onToggleBranch(typed);
+									setManual("");
+								}}
+							>
+								Hinzufügen
+							</Button>
+						</div>
+
+						<div className="space-y-2">
+							<Label>Ausgewählt ({selected.length})</Label>
+
+							{selected.length === 0 ? (
 								<p className="text-xs text-muted-foreground">
-									Niederlassungen konnten nicht geladen werden. Sie können NLxx
-									manuell hinzufügen.
+									Keine Niederlassungen ausgewählt.
 								</p>
-
-								<div className="flex gap-2">
-									<Input
-										value={manual}
-										onChange={(e) => setManual(e.target.value)}
-										onKeyDown={(e) => {
-											if (e.key !== "Enter") return;
-											if (!canAddTyped) return;
-
-											e.preventDefault();
-											onToggleBranch(typed);
-											setManual("");
-										}}
-										placeholder="z. B. NL17"
-										disabled={isSubmitting}
-									/>
-									<Button
-										type="button"
-										variant="outline"
-										disabled={!canAddTyped || isSubmitting}
-										onClick={() => {
-											if (!canAddTyped) return;
-											onToggleBranch(typed);
-											setManual("");
-										}}
-									>
-										Hinzufügen
-									</Button>
+							) : (
+								<div className="flex flex-wrap gap-2">
+									{selected.map((b) => (
+										<Badge key={b} variant="secondary" className="gap-1">
+											<span>{b}</span>
+											<button
+												type="button"
+												className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-sm hover:bg-muted/60"
+												onClick={() => onToggleBranch(b)}
+												disabled={isSubmitting}
+												aria-label={`Entfernen: ${b}`}
+												title={`Entfernen: ${b}`}
+											>
+												<X className="h-3 w-3" />
+											</button>
+										</Badge>
+									))}
 								</div>
-							</div>
-						) : null}
+							)}
+						</div>
 					</div>
-
-					<div className="w-full sm:w-auto">
-						<Label>Ausgewählt ({selected.length})</Label>
-
-						{selected.length === 0 ? (
-							<p className="mt-2 text-xs text-muted-foreground">
-								Keine Niederlassungen ausgewählt.
-							</p>
-						) : (
-							<div className="mt-2 flex flex-wrap gap-2">
-								{selected.map((b) => (
-									<Badge key={b} variant="secondary" className="gap-1">
-										<span>{b}</span>
-										<button
-											type="button"
-											className="ml-1 inline-flex h-4 w-4 items-center justify-center rounded-sm hover:bg-muted/60"
-											onClick={() => onToggleBranch(b)}
+				) : null}
+
+				{listReady ? (
+					<div className="space-y-2">
+						<div className="text-xs text-muted-foreground">
+							Ausgewählt: {selected.length}
+						</div>
+
+						{/* 5 per row on md+ */}
+						<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-5">
+							{branchOptions.map((b) => {
+								const id = String(b);
+								const checked = selectedSet.has(id);
+
+								return (
+									<label
+										key={id}
+										className="flex items-center gap-2 rounded-md px-2 py-1 hover:bg-muted/50"
+									>
+										<input
+											type="checkbox"
+											checked={checked}
+											onChange={() => onToggleBranch(id)}
 											disabled={isSubmitting}
-											aria-label={`Entfernen: ${b}`}
-											title={`Entfernen: ${b}`}
-										>
-											<X className="h-3 w-3" />
-										</button>
-									</Badge>
-								))}
-							</div>
-						)}
+										/>
+										<span className="text-sm">{id}</span>
+									</label>
+								);
+							})}
+						</div>
 					</div>
-				</div>
+				) : null}
 			</div>
 		</div>
 	);