Skip to content

Commit ec4b433

Browse files
authored
Merge branch 'master' into DX-2224-darkmode-revision
2 parents 43e9bda + 631fc23 commit ec4b433

File tree

8 files changed

+122
-34
lines changed

8 files changed

+122
-34
lines changed

src/components/databrowser/components/databrowser-tabs.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,9 @@ function AddTabButton() {
301301
variant="secondary"
302302
size="icon-sm"
303303
onClick={handleAddTab}
304-
className="flex-shrink-0"
304+
className="flex-shrink-0 dark:bg-zinc-200"
305305
>
306-
<IconPlus className="text-zinc-500" size={16} />
306+
<IconPlus className="text-zinc-500 dark:text-zinc-600" size={16} />
307307
</Button>
308308
)
309309
}

src/components/databrowser/components/display/delete-alert-dialog.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,30 @@ export function DeleteAlertDialog({
1818
open,
1919
onOpenChange,
2020
deletionType,
21+
count = 1,
2122
}: {
2223
children?: React.ReactNode
2324
onDeleteConfirm: MouseEventHandler
2425
open?: boolean
2526
onOpenChange?: (open: boolean) => void
2627
deletionType: "item" | "key"
28+
count?: number
2729
}) {
30+
const isPlural = count > 1
31+
const itemLabel = deletionType === "item" ? "Item" : "Key"
32+
const itemsLabel = deletionType === "item" ? "Items" : "Keys"
33+
2834
return (
2935
<AlertDialog open={open} onOpenChange={onOpenChange}>
3036
{children && <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>}
3137

3238
<AlertDialogContent>
3339
<AlertDialogHeader>
3440
<AlertDialogTitle>
35-
{deletionType === "item" ? "Delete Item" : "Delete Key"}
41+
{isPlural ? `Delete ${count} ${itemsLabel}` : `Delete ${itemLabel}`}
3642
</AlertDialogTitle>
3743
<AlertDialogDescription className="mt-5">
38-
Are you sure you want to delete this {deletionType}?<br />
44+
Are you sure you want to delete {isPlural ? `these ${count} ${deletionType}s` : `this ${deletionType}`}?<br />
3945
This action cannot be undone.
4046
</AlertDialogDescription>
4147
</AlertDialogHeader>

src/components/databrowser/components/sidebar-context-menu.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,23 @@ import { DeleteAlertDialog } from "./display/delete-alert-dialog"
1818
export const SidebarContextMenu = ({ children }: PropsWithChildren) => {
1919
const { mutate: deleteKey } = useDeleteKey()
2020
const [isAlertOpen, setAlertOpen] = useState(false)
21-
const [dataKey, setDataKey] = useState("")
22-
const { addTab, setSelectedKey, selectTab, setSearch } = useDatabrowserStore()
23-
const { search: currentSearch } = useTab()
21+
const [contextKeys, setContextKeys] = useState<string[]>([])
22+
const { addTab, setSelectedKey: setSelectedKeyGlobal, selectTab, setSearch } = useDatabrowserStore()
23+
const { search: currentSearch, selectedKeys, setSelectedKey } = useTab()
2424

2525
return (
2626
<>
2727
<DeleteAlertDialog
2828
deletionType="key"
29+
count={contextKeys.length}
2930
open={isAlertOpen}
3031
onOpenChange={setAlertOpen}
3132
onDeleteConfirm={(e) => {
3233
e.stopPropagation()
33-
deleteKey(dataKey)
34+
// Delete all selected keys
35+
for (const key of contextKeys) {
36+
deleteKey(key)
37+
}
3438
setAlertOpen(false)
3539
}}
3640
/>
@@ -42,7 +46,16 @@ export const SidebarContextMenu = ({ children }: PropsWithChildren) => {
4246
const key = el.closest("[data-key]")
4347

4448
if (key && key instanceof HTMLElement && key.dataset.key !== undefined) {
45-
setDataKey(key.dataset.key)
49+
const clickedKey = key.dataset.key
50+
51+
// If right-clicking on a selected key, keep all selected keys
52+
if (selectedKeys.includes(clickedKey)) {
53+
setContextKeys(selectedKeys)
54+
} else {
55+
// If right-clicking on an unselected key, select only that key
56+
setSelectedKey(clickedKey)
57+
setContextKeys([clickedKey])
58+
}
4659
} else {
4760
throw new Error("Key not found")
4861
}
@@ -53,32 +66,34 @@ export const SidebarContextMenu = ({ children }: PropsWithChildren) => {
5366
<ContextMenuContent>
5467
<ContextMenuItem
5568
onClick={() => {
56-
navigator.clipboard.writeText(dataKey)
69+
navigator.clipboard.writeText(contextKeys[0])
5770
toast({
5871
description: "Key copied to clipboard",
5972
})
6073
}}
6174
className="gap-2"
75+
disabled={contextKeys.length !== 1}
6276
>
6377
<IconCopy size={16} />
6478
Copy key
6579
</ContextMenuItem>
6680
<ContextMenuItem
6781
onClick={() => {
6882
const newTabId = addTab()
69-
setSelectedKey(newTabId, dataKey)
83+
setSelectedKeyGlobal(newTabId, contextKeys[0])
7084
setSearch(newTabId, currentSearch)
7185
selectTab(newTabId)
7286
}}
7387
className="gap-2"
88+
disabled={contextKeys.length !== 1}
7489
>
7590
<IconExternalLink size={16} />
7691
Open in new tab
7792
</ContextMenuItem>
7893
<ContextMenuSeparator />
7994
<ContextMenuItem onClick={() => setAlertOpen(true)} className="gap-2">
8095
<IconTrash size={16} />
81-
Delete key
96+
{contextKeys.length > 1 ? `Delete ${contextKeys.length} keys` : "Delete key"}
8297
</ContextMenuItem>
8398
</ContextMenuContent>
8499
</ContextMenu>

src/components/databrowser/components/sidebar/keys-list.tsx

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useRef } from "react"
12
import { useTab } from "@/tab-provider"
23
import type { DataType, RedisKey } from "@/types"
34

@@ -10,12 +11,26 @@ import { SidebarContextMenu } from "../sidebar-context-menu"
1011

1112
export const KeysList = () => {
1213
const { keys } = useKeys()
14+
const lastClickedIndexRef = useRef<number | null>(null)
1315

1416
return (
1517
<SidebarContextMenu>
1618
<>
19+
{/* Since the selection border is overflowing, we need a px padding for the first item */}
20+
<div className="h-px" />
1721
{keys.map((data, i) => (
18-
<KeyItem key={data[0]} nextKey={keys.at(i + 1)?.[0] ?? ""} data={data} />
22+
<>
23+
<KeyItem
24+
key={data[0]}
25+
index={i}
26+
data={data}
27+
allKeys={keys}
28+
lastClickedIndexRef={lastClickedIndexRef}
29+
/>
30+
{i !== keys.length - 1 && (
31+
<div className="-z-10 mx-2 h-px bg-zinc-100 dark:bg-zinc-200" />
32+
)}
33+
</>
1934
))}
2035
</>
2136
</SidebarContextMenu>
@@ -32,31 +47,58 @@ const keyStyles = {
3247
stream: "border-green-400 !bg-green-50 text-green-900",
3348
} as Record<DataType, string>
3449

35-
const KeyItem = ({ data, nextKey }: { data: RedisKey; nextKey: string }) => {
36-
const { selectedKey, setSelectedKey } = useTab()
50+
const KeyItem = ({
51+
data,
52+
index,
53+
allKeys,
54+
lastClickedIndexRef,
55+
}: {
56+
data: RedisKey
57+
index: number
58+
allKeys: RedisKey[]
59+
lastClickedIndexRef: React.MutableRefObject<number | null>
60+
}) => {
61+
const { selectedKeys, setSelectedKeys, setSelectedKey } = useTab()
3762

3863
const [dataKey, dataType] = data
39-
const isKeySelected = selectedKey === dataKey
40-
const isNextKeySelected = selectedKey === nextKey
64+
const isKeySelected = selectedKeys.includes(dataKey)
65+
66+
const handleClick = (e: React.MouseEvent) => {
67+
if (e.shiftKey && lastClickedIndexRef.current !== null) {
68+
// Shift+Click: select range
69+
const start = Math.min(lastClickedIndexRef.current, index)
70+
const end = Math.max(lastClickedIndexRef.current, index)
71+
const rangeKeys = allKeys.slice(start, end + 1).map(([key]) => key)
72+
setSelectedKeys(rangeKeys)
73+
} else if (e.metaKey || e.ctrlKey) {
74+
// cmd/ctrl+click to toggle selection
75+
if (isKeySelected) {
76+
setSelectedKeys(selectedKeys.filter((k) => k !== dataKey))
77+
} else {
78+
setSelectedKeys([...selectedKeys, dataKey])
79+
}
80+
lastClickedIndexRef.current = index
81+
} else {
82+
// Regular click: select single key
83+
setSelectedKey(dataKey)
84+
lastClickedIndexRef.current = index
85+
}
86+
}
4187

4288
return (
4389
<Button
4490
data-key={dataKey}
4591
variant={isKeySelected ? "default" : "ghost"}
4692
className={cn(
4793
"relative flex h-10 w-full items-center justify-start gap-2 px-3 py-0 !ring-0 focus-visible:bg-zinc-50",
48-
"select-none border border-transparent text-left",
94+
"-my-px select-none border border-transparent text-left",
4995
isKeySelected && "shadow-sm",
5096
isKeySelected && keyStyles[dataType]
5197
)}
52-
onClick={() => setSelectedKey(dataKey)}
98+
onClick={handleClick}
5399
>
54100
<TypeTag variant={dataType} type="icon" />
55101
<p className="truncate whitespace-nowrap">{dataKey}</p>
56-
57-
{!isKeySelected && !isNextKeySelected && (
58-
<span className="absolute -bottom-px left-3 right-3 h-px bg-zinc-100" />
59-
)}
60102
</Button>
61103
)
62104
}

src/components/databrowser/components/sidebar/search-input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,11 @@ export const SearchInput = () => {
8383
<div className="relative grow">
8484
<Popover open={isFocus && filteredHistory.length > 0}>
8585
<PopoverTrigger asChild>
86-
<div>
86+
<div className="h-8 rounded-md rounded-l-none border border-zinc-300 font-normal">
8787
<Input
8888
ref={inputRef}
8989
placeholder="Search"
90-
className={"rounded-l-none border-zinc-300 font-normal"}
90+
className={"h-full rounded-l-none border-none pr-6"}
9191
onKeyDown={handleKeyDown}
9292
onChange={(e) => {
9393
setState(e.currentTarget.value)

src/components/databrowser/components/tab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const Tab = ({ id, isList }: { id: TabId; isList?: boolean }) => {
7575
e.stopPropagation()
7676
removeTab(id)
7777
}}
78-
className="p-1 text-zinc-300 transition-colors hover:text-zinc-500"
78+
className="p-1 text-zinc-300 transition-colors hover:text-zinc-500 dark:text-zinc-400"
7979
>
8080
<IconX size={16} />
8181
</button>

src/store.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const DatabrowserProvider = ({
4545
setItem: (_name, value) => storage.set(JSON.stringify(value)),
4646
removeItem: () => {},
4747
},
48-
version: 2,
48+
version: 3,
4949
// @ts-expect-error Reset the store for < v1
5050
migrate: (originalState, version) => {
5151
const state = originalState as DatabrowserStore
@@ -60,6 +60,23 @@ export const DatabrowserProvider = ({
6060
}
6161
}
6262

63+
if (version === 2) {
64+
// Migrate from selectedKey to selectedKeys
65+
return {
66+
...state,
67+
tabs: state.tabs.map(([id, data]) => {
68+
const oldData = data as any
69+
return [
70+
id,
71+
{
72+
...data,
73+
selectedKeys: oldData.selectedKey ? [oldData.selectedKey] : [],
74+
},
75+
]
76+
}),
77+
}
78+
}
79+
6380
return state
6481
},
6582
})
@@ -102,7 +119,7 @@ export type SelectedItem = {
102119

103120
export type TabData = {
104121
id: TabId
105-
selectedKey: string | undefined
122+
selectedKeys: string[]
106123
selectedListItem?: SelectedItem
107124

108125
search: SearchFilter
@@ -128,8 +145,9 @@ type DatabrowserStore = {
128145
closeAllButPinned: () => void
129146

130147
// Tab actions
131-
getSelectedKey: (tabId: TabId) => string | undefined
148+
getSelectedKeys: (tabId: TabId) => string[]
132149
setSelectedKey: (tabId: TabId, key: string | undefined) => void
150+
setSelectedKeys: (tabId: TabId, keys: string[]) => void
133151
setSelectedListItem: (tabId: TabId, item?: { key: string; isNew?: boolean }) => void
134152
setSearch: (tabId: TabId, search: SearchFilter) => void
135153
setSearchKey: (tabId: TabId, key: string) => void
@@ -150,7 +168,7 @@ const storeCreator: StateCreator<DatabrowserStore> = (set, get) => ({
150168

151169
const newTabData: TabData = {
152170
id,
153-
selectedKey: undefined,
171+
selectedKeys: [],
154172
search: { key: "", type: undefined },
155173
pinned: false,
156174
}
@@ -275,18 +293,22 @@ const storeCreator: StateCreator<DatabrowserStore> = (set, get) => ({
275293
set({ selectedTab: id })
276294
},
277295

278-
getSelectedKey: (tabId) => {
279-
return get().tabs.find(([id]) => id === tabId)?.[1]?.selectedKey
296+
getSelectedKeys: (tabId) => {
297+
return get().tabs.find(([id]) => id === tabId)?.[1]?.selectedKeys ?? []
280298
},
281299

282300
setSelectedKey: (tabId, key) => {
301+
get().setSelectedKeys(tabId, key ? [key] : [])
302+
},
303+
304+
setSelectedKeys: (tabId, keys) => {
283305
set((old) => {
284306
const tabIndex = old.tabs.findIndex(([id]) => id === tabId)
285307
if (tabIndex === -1) return old
286308

287309
const newTabs = [...old.tabs]
288310
const [, tabData] = newTabs[tabIndex]
289-
newTabs[tabIndex] = [tabId, { ...tabData, selectedKey: key, selectedListItem: undefined }]
311+
newTabs[tabIndex] = [tabId, { ...tabData, selectedKeys: keys, selectedListItem: undefined }]
290312

291313
return { ...old, tabs: newTabs }
292314
})

src/tab-provider.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const useTab = () => {
2323
selectedTab,
2424
tabs,
2525
setSelectedKey,
26+
setSelectedKeys,
2627
setSelectedListItem,
2728
setSearch,
2829
setSearchKey,
@@ -37,12 +38,14 @@ export const useTab = () => {
3738
return useMemo(
3839
() => ({
3940
active: selectedTab === tabId,
40-
selectedKey: tabData.selectedKey,
41+
selectedKey: tabData.selectedKeys[0], // Backwards compatibility - first selected key
42+
selectedKeys: tabData.selectedKeys,
4143
selectedListItem: tabData.selectedListItem,
4244
search: tabData.search,
4345
pinned: tabData.pinned,
4446

4547
setSelectedKey: (key: string | undefined) => setSelectedKey(tabId, key),
48+
setSelectedKeys: (keys: string[]) => setSelectedKeys(tabId, keys),
4649
setSelectedListItem: (item: SelectedItem | undefined) => setSelectedListItem(tabId, item),
4750
setSearch: (search: SearchFilter) => setSearch(tabId, search),
4851
setSearchKey: (key: string) => setSearchKey(tabId, key),

0 commit comments

Comments
 (0)