From 19535396ad2c729bf257dff77c7ba195bd5f9262 Mon Sep 17 00:00:00 2001 From: kenneth Date: Sun, 21 Sep 2025 15:12:05 +0000 Subject: [PATCH] fix: directory table multi select --- packages/convex/model/error.ts | 3 +- .../directory-content-table.tsx | 32 +++++-- packages/web/src/lib/keyboard.ts | 85 +++++++++++++++++++ packages/web/src/routes/__root.tsx | 3 + platform.ts | 78 +++++++++++++++++ 5 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 packages/web/src/lib/keyboard.ts create mode 100644 platform.ts diff --git a/packages/convex/model/error.ts b/packages/convex/model/error.ts index 117fb7d..87821d5 100644 --- a/packages/convex/model/error.ts +++ b/packages/convex/model/error.ts @@ -17,6 +17,7 @@ export function isApplicationError(error: unknown): error is ApplicationError { export function create(code: Code, message?: string) { return new ConvexError({ code, - message, + message: + code === Code.Internal ? "Internal application error" : message, }) } diff --git a/packages/web/src/directories/directory-page/directory-content-table.tsx b/packages/web/src/directories/directory-page/directory-content-table.tsx index ee4977f..8fc22da 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -35,6 +35,10 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { + isControlOrCommandKeyActive, + keyboardModifierAtom, +} from "@/lib/keyboard" import { TextFileIcon } from "../../components/icons/text-file-icon" import { Button } from "../../components/ui/button" import { LoadingSpinner } from "../../components/ui/loading-spinner" @@ -67,15 +71,18 @@ const columns: ColumnDef[] = [ header: ({ table }) => ( + onCheckedChange={(value) => { table.toggleAllPageRowsSelected(!!value) - } + }} aria-label="Select all" /> ), cell: ({ row }) => ( { + e.stopPropagation() + }} onCheckedChange={row.getToggleSelectedHandler()} aria-label="Select row" /> @@ -229,10 +236,25 @@ export function DirectoryContentTableContent() { }) const selectRow = (row: Row) => { - console.log("row.getIsSelected()", row.getIsSelected()) - if (!row.getIsSelected()) { - table.toggleAllPageRowsSelected(false) + const keyboardModifiers = store.get(keyboardModifierAtom) + const isMultiSelectMode = isControlOrCommandKeyActive(keyboardModifiers) + const isRowSelected = row.getIsSelected() + console.log({ isMultiSelectMode, isRowSelected }) + if (isRowSelected && isMultiSelectMode) { + row.toggleSelected(false) + } else if (isRowSelected && !isMultiSelectMode) { + table.setRowSelection({ + [row.id]: true, + }) row.toggleSelected(true) + } else if (!isRowSelected) { + if (isMultiSelectMode) { + row.toggleSelected(true) + } else { + table.setRowSelection({ + [row.id]: true, + }) + } } } diff --git a/packages/web/src/lib/keyboard.ts b/packages/web/src/lib/keyboard.ts new file mode 100644 index 0000000..251623b --- /dev/null +++ b/packages/web/src/lib/keyboard.ts @@ -0,0 +1,85 @@ +import { atom, useSetAtom } from "jotai" +import { useEffect } from "react" + +export enum KeyboardModifier { + Alt = "Alt", + Control = "Control", + Meta = "Meta", + Shift = "Shift", +} + +export const keyboardModifierAtom = atom(new Set()) + +const addKeyboardModifierAtom = atom( + null, + (get, set, modifier: KeyboardModifier) => { + const activeModifiers = get(keyboardModifierAtom) + const nextActiveModifiers = new Set(activeModifiers) + nextActiveModifiers.add(modifier) + set(keyboardModifierAtom, nextActiveModifiers) + }, +) +const removeKeyboardModifierAtom = atom( + null, + (get, set, modifier: KeyboardModifier) => { + const activeModifiers = get(keyboardModifierAtom) + const nextActiveModifiers = new Set(activeModifiers) + nextActiveModifiers.delete(modifier) + set(keyboardModifierAtom, nextActiveModifiers) + }, +) + +export function useKeyboardModifierListener() { + const addKeyboardModifier = useSetAtom(addKeyboardModifierAtom) + const removeKeyboardModifier = useSetAtom(removeKeyboardModifierAtom) + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case "Alt": + addKeyboardModifier(KeyboardModifier.Alt) + break + case "Control": + addKeyboardModifier(KeyboardModifier.Control) + break + case "Meta": + addKeyboardModifier(KeyboardModifier.Meta) + break + case "Shift": + addKeyboardModifier(KeyboardModifier.Shift) + break + } + } + const handleKeyUp = (event: KeyboardEvent) => { + switch (event.key) { + case "Alt": + removeKeyboardModifier(KeyboardModifier.Alt) + break + case "Control": + removeKeyboardModifier(KeyboardModifier.Control) + break + case "Meta": + removeKeyboardModifier(KeyboardModifier.Meta) + break + case "Shift": + removeKeyboardModifier(KeyboardModifier.Shift) + break + } + } + window.addEventListener("keydown", handleKeyDown) + window.addEventListener("keyup", handleKeyUp) + return () => { + window.removeEventListener("keydown", handleKeyDown) + window.removeEventListener("keyup", handleKeyUp) + } + }, [addKeyboardModifier, removeKeyboardModifier]) +} + +export function isControlOrCommandKeyActive( + keyboardModifiers: Set, +) { + return ( + keyboardModifiers.has(KeyboardModifier.Control) || + keyboardModifiers.has(KeyboardModifier.Meta) + ) +} diff --git a/packages/web/src/routes/__root.tsx b/packages/web/src/routes/__root.tsx index d0bf9b9..16e20a1 100644 --- a/packages/web/src/routes/__root.tsx +++ b/packages/web/src/routes/__root.tsx @@ -6,6 +6,7 @@ import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react" import { ConvexReactClient } from "convex/react" import { toast } from "sonner" import { formatError } from "@/lib/error" +import { useKeyboardModifierListener } from "@/lib/keyboard" export const Route = createRootRoute({ component: RootLayout, @@ -25,6 +26,8 @@ const queryClient = new QueryClient({ }) function RootLayout() { + useKeyboardModifierListener() + return ( 0) + ) { + _isIOS = true + _platform = Platform.iOS + _isMobile = /iPhone|iPod/.test(_userAgent) || /Mobi/.test(_userAgent) + } + // Android detection + else if (/Android/.test(_userAgent)) { + _isAndroid = true + _platform = Platform.Android + _isMobile = true + } + // Windows detection + else if (/Windows/.test(_userAgent)) { + _isWindows = true + _platform = Platform.Windows + _isMobile = /Mobi/.test(_userAgent) + } + // macOS detection + else if (/Macintosh|Mac OS X/.test(_userAgent)) { + _isMacOS = true + _platform = Platform.MacOS + _isMobile = false + } + // Linux detection + else if (/Linux/.test(_userAgent)) { + _isLinux = true + _platform = Platform.Linux + _isMobile = /Mobi/.test(_userAgent) + } + // Fallback - check for mobile + else { + _isMobile = /Mobi/.test(_userAgent) + } +} + +// Exported constants (computed once) +export const isWindows = _isWindows +export const isMacOS = _isMacOS +export const isLinux = _isLinux +export const isAndroid = _isAndroid +export const isIOS = _isIOS +export const isMobile = _isMobile +export const platform = _platform +export const userAgent = _userAgent