From e0c4d7dd03ebbb5faf467aeae11c6474202874eb Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Mon, 17 Nov 2025 19:26:25 +0300 Subject: [PATCH 1/5] feat: add multiple shift select and delete functionality for the keys list --- .../components/databrowser-tabs.tsx | 4 +- .../display/delete-alert-dialog.tsx | 10 +++- .../components/sidebar-context-menu.tsx | 31 ++++++++---- .../databrowser/components/sidebar/index.tsx | 2 +- .../components/sidebar/keys-list.tsx | 47 ++++++++++++++++--- src/components/databrowser/components/tab.tsx | 2 +- src/store.tsx | 36 +++++++++++--- src/tab-provider.tsx | 5 +- 8 files changed, 109 insertions(+), 28 deletions(-) diff --git a/src/components/databrowser/components/databrowser-tabs.tsx b/src/components/databrowser/components/databrowser-tabs.tsx index fb0b64b..cc4ee6a 100644 --- a/src/components/databrowser/components/databrowser-tabs.tsx +++ b/src/components/databrowser/components/databrowser-tabs.tsx @@ -301,9 +301,9 @@ function AddTabButton() { variant="secondary" size="icon-sm" onClick={handleAddTab} - className="flex-shrink-0" + className="flex-shrink-0 dark:bg-zinc-200" > - + ) } diff --git a/src/components/databrowser/components/display/delete-alert-dialog.tsx b/src/components/databrowser/components/display/delete-alert-dialog.tsx index 6fcce85..5e757b5 100644 --- a/src/components/databrowser/components/display/delete-alert-dialog.tsx +++ b/src/components/databrowser/components/display/delete-alert-dialog.tsx @@ -18,13 +18,19 @@ export function DeleteAlertDialog({ open, onOpenChange, deletionType, + count = 1, }: { children?: React.ReactNode onDeleteConfirm: MouseEventHandler open?: boolean onOpenChange?: (open: boolean) => void deletionType: "item" | "key" + count?: number }) { + const isPlural = count > 1 + const itemLabel = deletionType === "item" ? "Item" : "Key" + const itemsLabel = deletionType === "item" ? "Items" : "Keys" + return ( {children && {children}} @@ -32,10 +38,10 @@ export function DeleteAlertDialog({ - {deletionType === "item" ? "Delete Item" : "Delete Key"} + {isPlural ? `Delete ${count} ${itemsLabel}` : `Delete ${itemLabel}`} - Are you sure you want to delete this {deletionType}?
+ Are you sure you want to delete {isPlural ? `these ${count} ${deletionType}s` : `this ${deletionType}`}?
This action cannot be undone.
diff --git a/src/components/databrowser/components/sidebar-context-menu.tsx b/src/components/databrowser/components/sidebar-context-menu.tsx index efcab37..647888f 100644 --- a/src/components/databrowser/components/sidebar-context-menu.tsx +++ b/src/components/databrowser/components/sidebar-context-menu.tsx @@ -18,19 +18,23 @@ import { DeleteAlertDialog } from "./display/delete-alert-dialog" export const SidebarContextMenu = ({ children }: PropsWithChildren) => { const { mutate: deleteKey } = useDeleteKey() const [isAlertOpen, setAlertOpen] = useState(false) - const [dataKey, setDataKey] = useState("") - const { addTab, setSelectedKey, selectTab, setSearch } = useDatabrowserStore() - const { search: currentSearch } = useTab() + const [contextKeys, setContextKeys] = useState([]) + const { addTab, setSelectedKey: setSelectedKeyGlobal, selectTab, setSearch } = useDatabrowserStore() + const { search: currentSearch, selectedKeys, setSelectedKey } = useTab() return ( <> { e.stopPropagation() - deleteKey(dataKey) + // Delete all selected keys + for (const key of contextKeys) { + deleteKey(key) + } setAlertOpen(false) }} /> @@ -42,7 +46,16 @@ export const SidebarContextMenu = ({ children }: PropsWithChildren) => { const key = el.closest("[data-key]") if (key && key instanceof HTMLElement && key.dataset.key !== undefined) { - setDataKey(key.dataset.key) + const clickedKey = key.dataset.key + + // If right-clicking on a selected key, keep all selected keys + if (selectedKeys.includes(clickedKey)) { + setContextKeys(selectedKeys) + } else { + // If right-clicking on an unselected key, select only that key + setSelectedKey(clickedKey) + setContextKeys([clickedKey]) + } } else { throw new Error("Key not found") } @@ -53,12 +66,13 @@ export const SidebarContextMenu = ({ children }: PropsWithChildren) => { { - navigator.clipboard.writeText(dataKey) + navigator.clipboard.writeText(contextKeys[0]) toast({ description: "Key copied to clipboard", }) }} className="gap-2" + disabled={contextKeys.length !== 1} > Copy key @@ -66,11 +80,12 @@ export const SidebarContextMenu = ({ children }: PropsWithChildren) => { { const newTabId = addTab() - setSelectedKey(newTabId, dataKey) + setSelectedKeyGlobal(newTabId, contextKeys[0]) setSearch(newTabId, currentSearch) selectTab(newTabId) }} className="gap-2" + disabled={contextKeys.length !== 1} > Open in new tab @@ -78,7 +93,7 @@ export const SidebarContextMenu = ({ children }: PropsWithChildren) => { setAlertOpen(true)} className="gap-2"> - Delete key + {contextKeys.length > 1 ? `Delete ${contextKeys.length} keys` : "Delete key"} diff --git a/src/components/databrowser/components/sidebar/index.tsx b/src/components/databrowser/components/sidebar/index.tsx index 16909c8..d735a67 100644 --- a/src/components/databrowser/components/sidebar/index.tsx +++ b/src/components/databrowser/components/sidebar/index.tsx @@ -28,7 +28,7 @@ export function Sidebar() {
diff --git a/src/store.tsx b/src/store.tsx index 26e65a7..0920a98 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -45,7 +45,7 @@ export const DatabrowserProvider = ({ setItem: (_name, value) => storage.set(JSON.stringify(value)), removeItem: () => {}, }, - version: 2, + version: 3, // @ts-expect-error Reset the store for < v1 migrate: (originalState, version) => { const state = originalState as DatabrowserStore @@ -60,6 +60,23 @@ export const DatabrowserProvider = ({ } } + if (version === 2) { + // Migrate from selectedKey to selectedKeys + return { + ...state, + tabs: state.tabs.map(([id, data]) => { + const oldData = data as any + return [ + id, + { + ...data, + selectedKeys: oldData.selectedKey ? [oldData.selectedKey] : [], + }, + ] + }), + } + } + return state }, }) @@ -102,7 +119,7 @@ export type SelectedItem = { export type TabData = { id: TabId - selectedKey: string | undefined + selectedKeys: string[] selectedListItem?: SelectedItem search: SearchFilter @@ -128,8 +145,9 @@ type DatabrowserStore = { closeAllButPinned: () => void // Tab actions - getSelectedKey: (tabId: TabId) => string | undefined + getSelectedKeys: (tabId: TabId) => string[] setSelectedKey: (tabId: TabId, key: string | undefined) => void + setSelectedKeys: (tabId: TabId, keys: string[]) => void setSelectedListItem: (tabId: TabId, item?: { key: string; isNew?: boolean }) => void setSearch: (tabId: TabId, search: SearchFilter) => void setSearchKey: (tabId: TabId, key: string) => void @@ -150,7 +168,7 @@ const storeCreator: StateCreator = (set, get) => ({ const newTabData: TabData = { id, - selectedKey: undefined, + selectedKeys: [], search: { key: "", type: undefined }, pinned: false, } @@ -275,18 +293,22 @@ const storeCreator: StateCreator = (set, get) => ({ set({ selectedTab: id }) }, - getSelectedKey: (tabId) => { - return get().tabs.find(([id]) => id === tabId)?.[1]?.selectedKey + getSelectedKeys: (tabId) => { + return get().tabs.find(([id]) => id === tabId)?.[1]?.selectedKeys ?? [] }, setSelectedKey: (tabId, key) => { + get().setSelectedKeys(tabId, key ? [key] : []) + }, + + setSelectedKeys: (tabId, keys) => { set((old) => { const tabIndex = old.tabs.findIndex(([id]) => id === tabId) if (tabIndex === -1) return old const newTabs = [...old.tabs] const [, tabData] = newTabs[tabIndex] - newTabs[tabIndex] = [tabId, { ...tabData, selectedKey: key, selectedListItem: undefined }] + newTabs[tabIndex] = [tabId, { ...tabData, selectedKeys: keys, selectedListItem: undefined }] return { ...old, tabs: newTabs } }) diff --git a/src/tab-provider.tsx b/src/tab-provider.tsx index cfdd1d7..3599ce8 100644 --- a/src/tab-provider.tsx +++ b/src/tab-provider.tsx @@ -23,6 +23,7 @@ export const useTab = () => { selectedTab, tabs, setSelectedKey, + setSelectedKeys, setSelectedListItem, setSearch, setSearchKey, @@ -37,12 +38,14 @@ export const useTab = () => { return useMemo( () => ({ active: selectedTab === tabId, - selectedKey: tabData.selectedKey, + selectedKey: tabData.selectedKeys[0], // Backwards compatibility - first selected key + selectedKeys: tabData.selectedKeys, selectedListItem: tabData.selectedListItem, search: tabData.search, pinned: tabData.pinned, setSelectedKey: (key: string | undefined) => setSelectedKey(tabId, key), + setSelectedKeys: (keys: string[]) => setSelectedKeys(tabId, keys), setSelectedListItem: (item: SelectedItem | undefined) => setSelectedListItem(tabId, item), setSearch: (search: SearchFilter) => setSearch(tabId, search), setSearchKey: (key: string) => setSearchKey(tabId, key), From 43dcb02f6c09f419e156f6c913537e537fb17acb Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Mon, 17 Nov 2025 19:46:10 +0300 Subject: [PATCH 2/5] fix: the double border issue in the keys list when multiple selecting --- .../components/sidebar/keys-list.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/databrowser/components/sidebar/keys-list.tsx b/src/components/databrowser/components/sidebar/keys-list.tsx index 4b4f6e3..04a0dc5 100644 --- a/src/components/databrowser/components/sidebar/keys-list.tsx +++ b/src/components/databrowser/components/sidebar/keys-list.tsx @@ -16,15 +16,21 @@ export const KeysList = () => { return ( <> + {/* Since the selection border is overflowing, we need a px padding for the first item */} +
{keys.map((data, i) => ( - + <> + + {i !== keys.length - 1 && ( +
+ )} + ))} @@ -43,13 +49,11 @@ const keyStyles = { const KeyItem = ({ data, - nextKey, index, allKeys, lastClickedIndexRef, }: { data: RedisKey - nextKey: string index: number allKeys: RedisKey[] lastClickedIndexRef: React.MutableRefObject @@ -58,7 +62,6 @@ const KeyItem = ({ const [dataKey, dataType] = data const isKeySelected = selectedKeys.includes(dataKey) - const isNextKeySelected = selectedKeys.includes(nextKey) const handleClick = (e: React.MouseEvent) => { if (e.shiftKey && lastClickedIndexRef.current !== null) { @@ -80,7 +83,7 @@ const KeyItem = ({ variant={isKeySelected ? "default" : "ghost"} className={cn( "relative flex h-10 w-full items-center justify-start gap-2 px-3 py-0 !ring-0 focus-visible:bg-zinc-50", - "select-none border border-transparent text-left", + "-my-px select-none border border-transparent text-left", isKeySelected && "shadow-sm", isKeySelected && keyStyles[dataType] )} @@ -88,10 +91,6 @@ const KeyItem = ({ >

{dataKey}

- - {!isKeySelected && !isNextKeySelected && ( - - )} ) } From e37b6830151cce8d5b6c70f20c8841675afa452f Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Mon, 17 Nov 2025 19:46:22 +0300 Subject: [PATCH 3/5] fix: search input content going under the "X" button --- .../databrowser/components/sidebar/search-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/databrowser/components/sidebar/search-input.tsx b/src/components/databrowser/components/sidebar/search-input.tsx index d4472a4..b901200 100644 --- a/src/components/databrowser/components/sidebar/search-input.tsx +++ b/src/components/databrowser/components/sidebar/search-input.tsx @@ -83,11 +83,11 @@ export const SearchInput = () => {
0}> -
+
{ setState(e.currentTarget.value) From 7e95e9ccdc37545415d0b85f6365218194c532f3 Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Wed, 19 Nov 2025 11:21:11 +0300 Subject: [PATCH 4/5] feat: add cmd/ctrl to multiple select --- .../databrowser/components/sidebar/keys-list.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/databrowser/components/sidebar/keys-list.tsx b/src/components/databrowser/components/sidebar/keys-list.tsx index 04a0dc5..daf7434 100644 --- a/src/components/databrowser/components/sidebar/keys-list.tsx +++ b/src/components/databrowser/components/sidebar/keys-list.tsx @@ -70,6 +70,14 @@ const KeyItem = ({ const end = Math.max(lastClickedIndexRef.current, index) const rangeKeys = allKeys.slice(start, end + 1).map(([key]) => key) setSelectedKeys(rangeKeys) + } else if (e.metaKey || e.ctrlKey) { + // cmd/ctrl+click to toggle selection + if (isKeySelected) { + setSelectedKeys(selectedKeys.filter((k) => k !== dataKey)) + } else { + setSelectedKeys([...selectedKeys, dataKey]) + } + lastClickedIndexRef.current = index } else { // Regular click: select single key setSelectedKey(dataKey) From 639989c236703394a942130f79bdead7955968bc Mon Sep 17 00:00:00 2001 From: ytkimirti Date: Mon, 24 Nov 2025 14:37:19 +0300 Subject: [PATCH 5/5] feat: add "cursor-pointer" to dropdown menu items --- src/components/ui/dropdown-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 97fca80..4f7d255 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -80,7 +80,7 @@ const DropdownMenuItem = React.forwardRef< svg]:size-4 relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none [&>svg]:shrink-0", + "data-[disabled]:opacity-50[&>svg]:size-4 relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-zinc-100 focus:text-zinc-900 data-[disabled]:pointer-events-none [&>svg]:shrink-0", inset && "pl-8", className )}