diff --git a/src/components/admin/bulkActions/MultiSelectDropdown.tsx b/src/components/admin/bulkActions/MultiSelectDropdown.tsx new file mode 100644 index 0000000..e2fc165 --- /dev/null +++ b/src/components/admin/bulkActions/MultiSelectDropdown.tsx @@ -0,0 +1,164 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { cn } from '@/lib/utils'; +import { ChevronDownIcon } from './DropdownSelect'; + +// ────────────────────────────────────────────────────────────────── +// MultiSelectDropdown +// +// Pop-over multi-select used by AdminBulkActions filters (tariffs, +// statuses, nodes, etc.). Closes on outside click; provides +// select-all / deselect-all helpers. Pure controlled component. +// ────────────────────────────────────────────────────────────────── + +export interface MultiSelectOption { + value: number; + label: string; +} + +export interface MultiSelectDropdownProps { + options: MultiSelectOption[]; + selected: number[]; + onChange: (ids: number[]) => void; + placeholder: string; + className?: string; +} + +export function MultiSelectDropdown({ + options, + selected, + onChange, + placeholder, + className, +}: MultiSelectDropdownProps) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + + useEffect(() => { + if (!open) return; + const handler = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [open]); + + const buttonLabel = useMemo(() => { + if (selected.length === 0) return placeholder; + if (selected.length <= 2) { + return selected + .map((id) => options.find((o) => o.value === id)?.label) + .filter(Boolean) + .join(', '); + } + return t('admin.bulkActions.filters.tariffsSelected', { count: selected.length }); + }, [selected, options, placeholder, t]); + + const handleToggle = (value: number) => { + if (selected.includes(value)) { + onChange(selected.filter((id) => id !== value)); + } else { + onChange([...selected, value]); + } + }; + + const handleSelectAll = () => { + onChange(options.map((o) => o.value)); + }; + + const handleDeselectAll = () => { + onChange([]); + }; + + return ( +
+ + + {open && ( +
+
+ + / + +
+ + {options.map((option) => { + const isChecked = selected.includes(option.value); + return ( + + ); + })} +
+ )} +
+ ); +} diff --git a/src/pages/AdminBulkActions.tsx b/src/pages/AdminBulkActions.tsx index 2a05ec2..f581219 100644 --- a/src/pages/AdminBulkActions.tsx +++ b/src/pages/AdminBulkActions.tsx @@ -31,6 +31,10 @@ import { } from '@/components/admin/bulkActions/DropdownSelect'; import { FloatingActionBar } from '@/components/admin/bulkActions/FloatingActionBar'; import { isSubscriptionLevelAction } from '@/components/admin/bulkActions/actionTargets'; +import { + MultiSelectDropdown, + type MultiSelectOption, +} from '@/components/admin/bulkActions/MultiSelectDropdown'; // ============ Types ============ @@ -351,161 +355,7 @@ function ProgressBar({ loading }: { loading: boolean }) { // (DropdownSelect moved into ./bulkActions/DropdownSelect.tsx) -// ============ Multi-Select Dropdown ============ - -interface MultiSelectOption { - value: number; - label: string; -} - -interface MultiSelectDropdownProps { - options: MultiSelectOption[]; - selected: number[]; - onChange: (ids: number[]) => void; - placeholder: string; - className?: string; -} - -function MultiSelectDropdown({ - options, - selected, - onChange, - placeholder, - className, -}: MultiSelectDropdownProps) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const containerRef = useRef(null); - - useEffect(() => { - if (!open) return; - const handler = (e: MouseEvent) => { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setOpen(false); - } - }; - document.addEventListener('mousedown', handler); - return () => document.removeEventListener('mousedown', handler); - }, [open]); - - const buttonLabel = useMemo(() => { - if (selected.length === 0) return placeholder; - if (selected.length <= 2) { - return selected - .map((id) => options.find((o) => o.value === id)?.label) - .filter(Boolean) - .join(', '); - } - return t('admin.bulkActions.filters.tariffsSelected', { count: selected.length }); - }, [selected, options, placeholder, t]); - - const handleToggle = (value: number) => { - if (selected.includes(value)) { - onChange(selected.filter((id) => id !== value)); - } else { - onChange([...selected, value]); - } - }; - - const handleSelectAll = () => { - onChange(options.map((o) => o.value)); - }; - - const handleDeselectAll = () => { - onChange([]); - }; - - return ( -
- - - {open && ( -
- {/* Select all / Deselect all */} -
- - / - -
- - {/* Options */} - {options.map((option) => { - const isChecked = selected.includes(option.value); - return ( - - ); - })} -
- )} -
- ); -} +// (MultiSelectDropdown moved into ./bulkActions/MultiSelectDropdown.tsx) // (ProgressView moved into )