From 05406640f046d48bc289b4ff42beea55b0cc74f3 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:44:07 +0100 Subject: [PATCH] feat(base): floating selection action bar with bulk delete --- .../base/components/grid/grid-container.tsx | 2 + .../components/grid/selection-action-bar.tsx | 53 +++++++++++++++++++ .../base/hooks/use-delete-selected-rows.ts | 35 ++++++++++++ .../src/features/base/styles/grid.module.css | 27 ++++++++++ 4 files changed, 117 insertions(+) create mode 100644 apps/client/src/features/base/components/grid/selection-action-bar.tsx create mode 100644 apps/client/src/features/base/hooks/use-delete-selected-rows.ts diff --git a/apps/client/src/features/base/components/grid/grid-container.tsx b/apps/client/src/features/base/components/grid/grid-container.tsx index 3334a2e8..0b0cc47e 100644 --- a/apps/client/src/features/base/components/grid/grid-container.tsx +++ b/apps/client/src/features/base/components/grid/grid-container.tsx @@ -24,6 +24,7 @@ import { useRowDrag } from "@/features/base/hooks/use-row-drag"; import { GridHeader } from "./grid-header"; import { GridRow } from "./grid-row"; import { AddRowButton } from "./add-row-button"; +import { SelectionActionBar } from "./selection-action-bar"; import classes from "@/features/base/styles/grid.module.css"; const ROW_HEIGHT = 36; @@ -251,6 +252,7 @@ export function GridContainer({ )} + {baseId && } diff --git a/apps/client/src/features/base/components/grid/selection-action-bar.tsx b/apps/client/src/features/base/components/grid/selection-action-bar.tsx new file mode 100644 index 00000000..a60d9c99 --- /dev/null +++ b/apps/client/src/features/base/components/grid/selection-action-bar.tsx @@ -0,0 +1,53 @@ +import { memo } from "react"; +import { ActionIcon, Button, Transition } from "@mantine/core"; +import { IconTrash, IconX } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useRowSelection } from "@/features/base/hooks/use-row-selection"; +import { useDeleteSelectedRows } from "@/features/base/hooks/use-delete-selected-rows"; +import classes from "@/features/base/styles/grid.module.css"; + +type SelectionActionBarProps = { + baseId: string; +}; + +export const SelectionActionBar = memo(function SelectionActionBar({ + baseId, +}: SelectionActionBarProps) { + const { t } = useTranslation(); + const { selectionCount, clear } = useRowSelection(); + const { deleteSelected, isPending } = useDeleteSelectedRows(baseId); + + const isOpen = selectionCount > 0; + + return ( + + {(styles) => ( +
+
+ + {t("{{count}} selected", { count: selectionCount })} + + + + + +
+
+ )} +
+ ); +}); diff --git a/apps/client/src/features/base/hooks/use-delete-selected-rows.ts b/apps/client/src/features/base/hooks/use-delete-selected-rows.ts new file mode 100644 index 00000000..f52d716b --- /dev/null +++ b/apps/client/src/features/base/hooks/use-delete-selected-rows.ts @@ -0,0 +1,35 @@ +import { useCallback } from "react"; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; +import { useRowSelection } from "@/features/base/hooks/use-row-selection"; +import { useDeleteRowsMutation } from "@/features/base/queries/base-row-query"; + +const BATCH_SIZE = 500; + +export function useDeleteSelectedRows(baseId: string) { + const { t } = useTranslation(); + const { selectedIds, clear } = useRowSelection(); + const mutation = useDeleteRowsMutation(); + + const deleteSelected = useCallback(async () => { + const ids = Array.from(selectedIds); + if (ids.length === 0) return; + const chunks: string[][] = []; + for (let i = 0; i < ids.length; i += BATCH_SIZE) { + chunks.push(ids.slice(i, i + BATCH_SIZE)); + } + try { + for (const chunk of chunks) { + await mutation.mutateAsync({ baseId, rowIds: chunk }); + } + notifications.show({ + message: t("{{count}} rows deleted", { count: ids.length }), + }); + clear(); + } catch { + // mutation onError already shows notification + } + }, [baseId, selectedIds, mutation, clear, t]); + + return { deleteSelected, isPending: mutation.isPending }; +} diff --git a/apps/client/src/features/base/styles/grid.module.css b/apps/client/src/features/base/styles/grid.module.css index 73466d68..0a265481 100644 --- a/apps/client/src/features/base/styles/grid.module.css +++ b/apps/client/src/features/base/styles/grid.module.css @@ -374,3 +374,30 @@ .hasSelection .rowNumberHeaderCheckbox { display: inline-flex; } + +.selectionActionBarWrapper { + position: sticky; + bottom: 16px; + display: flex; + justify-content: center; + pointer-events: none; + z-index: 5; + grid-column: 1 / -1; +} + +.selectionActionBar { + pointer-events: auto; + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 10px; + border-radius: var(--mantine-radius-md); + box-shadow: var(--mantine-shadow-lg); + background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); +} + +.selectionActionBarCount { + font-size: var(--mantine-font-size-sm); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); +}