Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
]
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-alert-dialog": "^1.0.5",
Expand Down
72 changes: 72 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

145 changes: 138 additions & 7 deletions src/components/databrowser/components/databrowser-tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,157 @@
import { useEffect, useRef, useState } from "react"
import type { TabId } from "@/store"
import { useDatabrowserStore } from "@/store"
import { TabIdProvider } from "@/tab-provider"
import {
closestCenter,
DndContext,
MeasuringStrategy,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core"
import { restrictToHorizontalAxis } from "@dnd-kit/modifiers"
import { horizontalListSortingStrategy, SortableContext, useSortable } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

import { Tab } from "./tab"

const SortableTab = ({ id }: { id: TabId }) => {
const [originalWidth, setOriginalWidth] = useState<number | null>(null)
const textRef = useRef<HTMLElement | null>(null)

const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id,
resizeObserverConfig: {
disabled: true,
},
})

const measureRef = (element: HTMLDivElement | null) => {
if (element && !originalWidth) {
const width = element.getBoundingClientRect().width
setOriginalWidth(width)

if (element) {
const textSpan = element.querySelector("span")
if (textSpan) {
textRef.current = textSpan as HTMLElement
}
}
}
setNodeRef(element)
}
useEffect(() => {
if (textRef.current && isDragging) {
const originalMaxWidth = textRef.current.style.maxWidth
const originalWhiteSpace = textRef.current.style.whiteSpace
const originalOverflow = textRef.current.style.overflow
const originalTextOverflow = textRef.current.style.textOverflow

textRef.current.style.maxWidth = "none"
textRef.current.style.whiteSpace = "nowrap"
textRef.current.style.overflow = "visible"
textRef.current.style.textOverflow = "clip"

return () => {
if (textRef.current) {
textRef.current.style.maxWidth = originalMaxWidth
textRef.current.style.whiteSpace = originalWhiteSpace
textRef.current.style.overflow = originalOverflow
textRef.current.style.textOverflow = originalTextOverflow
}
}
}
}, [isDragging])
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
if (entries[0]) {
setOriginalWidth(entries[0].contentRect.width)
}
})

return () => resizeObserver.disconnect()
}, [])

const style = {
transform: transform
? CSS.Transform.toString({
...transform,
y: 0,
scaleX: 1,
scaleY: 1,
})
: "",
transition,
...(isDragging
? {
zIndex: 50,
minWidth: originalWidth ? `${originalWidth}px` : undefined,
}
: {}),
}

return (
<div
ref={measureRef}
style={style}
className={isDragging ? "cursor-grabbing" : "cursor-grab"}
{...attributes}
{...listeners}
>
<TabIdProvider value={id as TabId}>
<Tab id={id} />
</TabIdProvider>
</div>
)
}

export const DatabrowserTabs = () => {
const { tabs, addTab, selectedTab } = useDatabrowserStore()
const { tabs, addTab, reorderTabs, selectedTab } = useDatabrowserStore()

const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 5,
},
})
)

const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event

if (over && active.id !== over.id) {
const oldIndex = tabs.findIndex(([id]) => id === active.id)
const newIndex = tabs.findIndex(([id]) => id === over.id)

reorderTabs(oldIndex, newIndex)
}
}

return (
<div className="relative mb-2 shrink-0">
<div className="absolute bottom-0 left-0 right-0 -z-10 h-[1px] w-full bg-zinc-200" />

<div className="scrollbar-hide flex translate-y-[1px] items-center gap-1 overflow-x-scroll pb-[1px] [&::-webkit-scrollbar]:hidden">
{selectedTab &&
tabs.map(([id]) => (
<TabIdProvider key={id} value={id as TabId}>
<Tab id={id} />
</TabIdProvider>
))}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
modifiers={[restrictToHorizontalAxis]}
measuring={{
droppable: {
strategy: MeasuringStrategy.Always,
},
}}
>
<SortableContext items={tabs.map(([id]) => id)} strategy={horizontalListSortingStrategy}>
{selectedTab && tabs.map(([id]) => <SortableTab key={id} id={id} />)}
</SortableContext>
</DndContext>
<Button
variant="secondary"
size="icon-sm"
Expand Down
10 changes: 10 additions & 0 deletions src/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type DatabrowserStore = {
addTab: () => TabId
removeTab: (id: TabId) => void
selectTab: (id: TabId) => void
reorderTabs: (oldIndex: number, newIndex: number) => void

// Tab actions
getSelectedKey: (tabId: TabId) => string | undefined
Expand Down Expand Up @@ -133,6 +134,15 @@ const storeCreator: StateCreator<DatabrowserStore> = (set, get) => ({
return id
},

reorderTabs: (oldIndex, newIndex) => {
set((old) => {
const newTabs = [...old.tabs]
const [movedTab] = newTabs.splice(oldIndex, 1)
newTabs.splice(newIndex, 0, movedTab)
return { ...old, tabs: newTabs }
})
},

removeTab: (id) => {
set((old) => {
const tabIndex = old.tabs.findIndex(([tabId]) => tabId === id)
Expand Down
18 changes: 9 additions & 9 deletions tests/test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,21 @@ describe("keys", () => {
await page.getByRole("textbox", { name: "Search" }).fill("mykey-13")
await page.getByRole("textbox", { name: "Search" }).press("Enter")

await page.getByRole("button", { name: "mykey-13" }).click()
await page.getByRole("button", { name: "mykey-13", exact: true }).click()

await page.getByRole("button", { name: "Key actions" }).click()
await page.getByRole("menuitem", { name: "Delete key" }).click()
await page.getByRole("button", { name: "Cancel" }).press("Escape")

await page.getByRole("button", { name: "mykey-13" }).click()
await page.getByRole("button", { name: "mykey-13", exact: true }).click()
})

test("can delete a key", async ({ page }) => {
await page.getByRole("textbox", { name: "Search" }).click()
await page.getByRole("textbox", { name: "Search" }).fill("mykey-13")
await page.getByRole("textbox", { name: "Search" }).press("Enter")

await page.getByRole("button", { name: "mykey-13" }).click()
await page.getByRole("button", { name: "mykey-13", exact: true }).click()

await page.getByRole("button", { name: "Key actions" }).click()
await page.getByRole("menuitem", { name: "Delete key" }).click()
Expand All @@ -73,7 +73,7 @@ describe("keys", () => {

await markDatabaseAsModified()

await expect(page.getByRole("button", { name: "mykey-13" })).not.toBeVisible()
await expect(page.getByRole("button", { name: "mykey-13", exact: true })).not.toBeVisible()
})

test("can add a string key", async ({ page }) => {
Expand Down Expand Up @@ -127,7 +127,7 @@ describe("hash", () => {
await page.getByRole("textbox", { name: "Search" }).click()
await page.getByRole("textbox", { name: "Search" }).fill("myhash")
await page.getByRole("textbox", { name: "Search" }).press("Enter")
await page.getByRole("button", { name: "myhash" }).click()
await page.getByRole("button", { name: "myhash", exact: true }).click()

await page.getByRole("cell", { name: "field-10" }).click()

Expand All @@ -146,7 +146,7 @@ describe("hash", () => {
await page.getByRole("textbox", { name: "Search" }).click()
await page.getByRole("textbox", { name: "Search" }).fill("myhash")
await page.getByRole("textbox", { name: "Search" }).press("Enter")
await page.getByRole("button", { name: "myhash" }).click()
await page.getByRole("button", { name: "myhash", exact: true }).click()

await page.getByRole("cell", { name: "field-10" }).click()

Expand Down Expand Up @@ -179,7 +179,7 @@ describe("hash", () => {
await page.getByRole("textbox", { name: "Search" }).click()
await page.getByRole("textbox", { name: "Search" }).fill("myhash")
await page.getByRole("textbox", { name: "Search" }).press("Enter")
await page.getByRole("button", { name: "myhash" }).click()
await page.getByRole("button", { name: "myhash", exact: true }).click()

await page.getByRole("row", { name: "field-10 value-10" }).getByRole("button").click()
await page.getByRole("button", { name: "Yes, Delete" }).click()
Expand All @@ -195,14 +195,14 @@ describe("tabs", () => {
await page.getByRole("textbox", { name: "Search" }).click()
await page.getByRole("textbox", { name: "Search" }).fill("mykey-42")
await page.getByRole("textbox", { name: "Search" }).press("Enter")
await page.getByRole("button", { name: "mykey-42" }).click()
await page.getByRole("button", { name: "mykey-42", exact: true }).click()

await page.getByRole("button", { name: "Add new tab" }).click()

await page.getByRole("textbox", { name: "Search" }).click()
await page.getByRole("textbox", { name: "Search" }).fill("mykey-13")
await page.getByRole("textbox", { name: "Search" }).press("Enter")
await page.getByRole("button", { name: "mykey-13" }).click()
await page.getByRole("button", { name: "mykey-13", exact: true }).click()

// Changes to the first tab
await page.getByText("mykey-42*").click()
Expand Down