Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ComponentProps } from 'react'
import type { ComponentProps, KeyboardEvent, MouseEvent } from 'react'
import type { TableCellProps, TableHeaderCellProps } from '~/components/table'
import { cn } from '@conar/ui/lib/utils'
import { RiCheckLine, RiSubtractLine } from '@remixicon/react'
import { useStore } from '@tanstack/react-store'
import { useRef } from 'react'
import { useTableContext } from '~/components/table'
import { usePageStoreContext } from '../-store'
import { useLastClickedIndexRef, usePageStoreContext, useSelectionStateRef } from '../-store'

function IndeterminateCheckbox({
indeterminate,
Expand Down Expand Up @@ -79,26 +80,75 @@ export function SelectionCell({ rowIndex, columnIndex, className, size, keys }:
}) {
const store = usePageStoreContext()
const rows = useTableContext(state => state.rows)
const lastClickedIndexRef = useLastClickedIndexRef()
const selectionStateRef = useSelectionStateRef()
const shiftKeyRef = useRef(false)
const isSelected = useStore(store, state => state.selected.some(row => keys.every(key => row[key] === rows[rowIndex]![key])))

const handleMouseDown = (event: MouseEvent<HTMLInputElement>) => {
shiftKeyRef.current = event.shiftKey
}

const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === ' ' || event.key === 'Enter') {
shiftKeyRef.current = event.shiftKey
}
}

const handleChange = () => {
const lastIndex = lastClickedIndexRef.current
const isShiftHeld = shiftKeyRef.current

if (isShiftHeld && lastIndex !== null && lastIndex !== rowIndex) {
const start = Math.min(lastIndex, rowIndex)
const end = Math.max(lastIndex, rowIndex)

const rangeRows = rows.slice(start, end + 1)
const rangeKeys = rangeRows.map(row =>
keys.reduce<Record<string, string>>((acc, key) => ({ ...acc, [key]: row[key] as string }), {}),
)

store.setState(state => ({
...state,
selected: rangeKeys,
} satisfies typeof state))

selectionStateRef.current = {
anchorIndex: lastIndex,
focusIndex: rowIndex,
lastExpandDirection: rowIndex > lastIndex ? 'down' : 'up',
}
}
else {
if (isSelected) {
store.setState(state => ({
...state,
selected: store.state.selected.filter(row => !keys.every(key => row[key] === rows[rowIndex]![key])),
} satisfies typeof state))

selectionStateRef.current = { anchorIndex: null, focusIndex: null, lastExpandDirection: null }
}
else {
store.setState(state => ({
...state,
selected: [...state.selected, keys.reduce((acc, key) => ({ ...acc, [key]: rows[rowIndex]![key] }), {})],
} satisfies typeof state))

selectionStateRef.current = { anchorIndex: rowIndex, focusIndex: rowIndex, lastExpandDirection: null }
}
}

lastClickedIndexRef.current = rowIndex
shiftKeyRef.current = false
}

return (
<div className={cn('flex items-center w-fit', columnIndex === 0 && 'pl-4', className)} style={{ width: `${size}px` }}>
<IndeterminateCheckbox
checked={isSelected}
onChange={() => {
if (isSelected) {
store.setState(state => ({
...state,
selected: store.state.selected.filter(row => !keys.every(key => row[key] === rows[rowIndex]![key])),
} satisfies typeof state))
}
else {
store.setState(state => ({
...state,
selected: [...state.selected, keys.reduce((acc, key) => ({ ...acc, [key]: rows[rowIndex]![key] }), {})],
} satisfies typeof state))
}
}}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
onChange={handleChange}
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { KeyboardEvent } from 'react'
import type { ColumnRenderer } from '~/components/table'
import { SQL_FILTERS_LIST } from '@conar/shared/filters/sql'
import { useInfiniteQuery } from '@tanstack/react-query'
Expand All @@ -12,7 +13,7 @@ import { queryClient } from '~/main'
import { Route } from '..'
import { getColumnSize, selectSymbol } from '../-lib'
import { useTableColumns } from '../-queries/use-columns-query'
import { usePageStoreContext } from '../-store'
import { usePageStoreContext, useSelectionStateRef } from '../-store'
import { useHeaderActionsOrder } from './header-actions-order'
import { TableEmpty } from './table-empty'
import { TableHeader } from './table-header'
Expand Down Expand Up @@ -47,6 +48,7 @@ function TableComponent({ table, schema }: { table: string, schema: string }) {
const { database } = Route.useLoaderData()
const columns = useTableColumns({ database, table, schema })
const store = usePageStoreContext()
const selectionStateRef = useSelectionStateRef()
const hiddenColumns = useStore(store, state => state.hiddenColumns)
const [filters, orderBy] = useStore(store, state => [state.filters, state.orderBy])
const { data: rows, error, isPending: isRowsPending } = useInfiniteQuery(databaseRowsQuery({ database, table, schema, query: { filters, orderBy } }))
Expand Down Expand Up @@ -225,14 +227,107 @@ function TableComponent({ table, schema }: { table: string, schema: string }) {
return sortedColumns
}, [columns, hiddenColumns, primaryColumns, saveValue, onOrder])

const handleKeyDown = useCallback((event: KeyboardEvent<HTMLDivElement>) => {
if (!event.shiftKey || !rows || rows.length === 0 || primaryColumns.length === 0)
return

const isArrowDown = event.key === 'ArrowDown'
const isArrowUp = event.key === 'ArrowUp'

if (!isArrowDown && !isArrowUp)
return

event.preventDefault()

const { anchorIndex, focusIndex } = selectionStateRef.current
const currentDirection = isArrowDown ? 'down' : 'up'

if (anchorIndex === null || focusIndex === null) {
const startIndex = isArrowDown ? 0 : rows.length - 1
selectionStateRef.current = { anchorIndex: startIndex, focusIndex: startIndex, lastExpandDirection: null }

const rowKeys = primaryColumns.reduce<Record<string, string>>(
(acc, key) => ({ ...acc, [key]: rows[startIndex]![key] as string }),
{},
)

store.setState(state => ({
...state,
selected: [rowKeys],
} satisfies typeof state))
return
}

const newFocusIndex = isArrowDown
? Math.min(focusIndex + 1, rows.length - 1)
: Math.max(focusIndex - 1, 0)

const atBoundary = newFocusIndex === focusIndex

if (anchorIndex === focusIndex) {
if (atBoundary)
return

selectionStateRef.current = { anchorIndex, focusIndex: newFocusIndex, lastExpandDirection: currentDirection }

const start = Math.min(anchorIndex, newFocusIndex)
const end = Math.max(anchorIndex, newFocusIndex)
const rangeRows = rows.slice(start, end + 1)
const rangeKeys = rangeRows.map(row =>
primaryColumns.reduce<Record<string, string>>(
(acc, key) => ({ ...acc, [key]: row[key] as string }),
{},
),
)

store.setState(state => ({
...state,
selected: rangeKeys,
} satisfies typeof state))
return
}

if (atBoundary)
return

const wasExpandedDown = focusIndex > anchorIndex
const wasExpandedUp = focusIndex < anchorIndex
const isShrinking = (wasExpandedDown && isArrowUp) || (wasExpandedUp && isArrowDown)

selectionStateRef.current.focusIndex = newFocusIndex
if (!isShrinking) {
selectionStateRef.current.lastExpandDirection = currentDirection
}

const start = Math.min(anchorIndex, newFocusIndex)
const end = Math.max(anchorIndex, newFocusIndex)

const rangeRows = rows.slice(start, end + 1)
const rangeKeys = rangeRows.map(row =>
primaryColumns.reduce<Record<string, string>>(
(acc, key) => ({ ...acc, [key]: row[key] as string }),
{},
),
)

store.setState(state => ({
...state,
selected: rangeKeys,
} satisfies typeof state))
}, [rows, primaryColumns, store, selectionStateRef])

return (
<TableProvider
rows={rows ?? []}
columns={tableColumns}
estimatedRowSize={DEFAULT_ROW_HEIGHT}
estimatedColumnSize={DEFAULT_COLUMN_WIDTH}
>
<div className="size-full relative bg-background">
<div
className="size-full relative bg-background outline-none"
tabIndex={0}
onKeyDown={handleKeyDown}
>
<Table>
<TableHeader />
{isRowsPending
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ActiveFilter, Filter } from '@conar/shared/filters'
import type { RefObject } from 'react'
import { Store } from '@tanstack/react-store'
import { type } from 'arktype'
import { createContext, use } from 'react'
Expand Down Expand Up @@ -63,3 +64,21 @@ export const PageStoreContext = createContext<Store<typeof storeState.infer>>(nu
export function usePageStoreContext() {
return use(PageStoreContext)
}

export const LastClickedIndexContext = createContext<RefObject<number | null>>(null!)

export function useLastClickedIndexRef() {
return use(LastClickedIndexContext)
}

export interface SelectionState {
anchorIndex: number | null
focusIndex: number | null
lastExpandDirection: 'up' | 'down' | null
}

export const SelectionStateContext = createContext<RefObject<SelectionState>>(null!)

export function useSelectionStateRef() {
return use(SelectionStateContext)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { ActiveFilter } from '@conar/shared/filters'
import type { Store } from '@tanstack/react-store'
import type { storeState } from './-store'
import type { SelectionState, storeState } from './-store'
import { SQL_FILTERS_GROUPED } from '@conar/shared/filters/sql'
import { title } from '@conar/shared/utils/title'
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@conar/ui/components/resizable'
import { createFileRoute } from '@tanstack/react-router'
import { useStore } from '@tanstack/react-store'
import { type } from 'arktype'
import { useEffect, useEffectEvent } from 'react'
import { useEffect, useEffectEvent, useRef } from 'react'
import { FiltersProvider } from '~/components/table'
import { addTab, databaseStore, prefetchDatabaseCore, prefetchDatabaseTableCore } from '~/entities/database'
import { Filters } from './-components/filters'
Expand All @@ -16,7 +16,7 @@ import { Sidebar } from './-components/sidebar'
import { Table } from './-components/table'
import { TablesTabs } from './-components/tabs'
import { useTableColumns } from './-queries/use-columns-query'
import { createPageStore, PageStoreContext } from './-store'
import { createPageStore, LastClickedIndexContext, PageStoreContext, SelectionStateContext } from './-store'

export const Route = createFileRoute(
'/(protected)/_protected/database/$id/table/',
Expand Down Expand Up @@ -68,6 +68,17 @@ export const Route = createFileRoute(
function TableContent({ table, schema, store }: { table: string, schema: string, store: Store<typeof storeState.infer> }) {
const { database } = Route.useLoaderData()
const deps = Route.useLoaderDeps()
const lastClickedIndexRef = useRef<number | null>(null)
const selectionStateRef = useRef<SelectionState>({ anchorIndex: null, focusIndex: null, lastExpandDirection: null })

const resetSelectionStateEvent = useEffectEvent(() => {
lastClickedIndexRef.current = null
selectionStateRef.current = { anchorIndex: null, focusIndex: null, lastExpandDirection: null }
})

useEffect(() => {
resetSelectionStateEvent()
}, [table, schema])

useEffect(() => {
Comment thread
geekyharsh05 marked this conversation as resolved.
if (store && (deps.filters || deps.orderBy)) {
Expand Down Expand Up @@ -106,29 +117,33 @@ function TableContent({ table, schema, store }: { table: string, schema: string,
}, [columns, store])

return (
<PageStoreContext value={store}>
<TablesTabs className="h-9" database={database} />
<div
key={table}
className="h-[calc(100%-(--spacing(9)))]"
onClick={() => addTab(database.id, schema, table)}
>
<FiltersProvider
columns={columns ?? []}
filtersGrouped={SQL_FILTERS_GROUPED}
>
<div className="h-full flex flex-col justify-between">
<div className="flex flex-col gap-4 px-4 pt-2 pb-4">
<Header table={table} schema={schema} />
<Filters />
</div>
<div className="flex-1 overflow-hidden">
<Table table={table} schema={schema} />
</div>
<SelectionStateContext value={selectionStateRef}>
<LastClickedIndexContext value={lastClickedIndexRef}>
<PageStoreContext value={store}>
<TablesTabs className="h-9" database={database} />
<div
key={table}
className="h-[calc(100%-(--spacing(9)))]"
onClick={() => addTab(database.id, schema, table)}
>
<FiltersProvider
columns={columns ?? []}
filtersGrouped={SQL_FILTERS_GROUPED}
>
<div className="h-full flex flex-col justify-between">
<div className="flex flex-col gap-4 px-4 pt-2 pb-4">
<Header table={table} schema={schema} />
<Filters />
</div>
<div className="flex-1 overflow-hidden">
<Table table={table} schema={schema} />
</div>
</div>
</FiltersProvider>
</div>
</FiltersProvider>
</div>
</PageStoreContext>
</PageStoreContext>
</LastClickedIndexContext>
</SelectionStateContext>
)
}

Expand Down