From 5508ec7116908eefb172a2c954270f77b4781e32 Mon Sep 17 00:00:00 2001 From: Kareem Ebrahim Date: Sun, 15 Sep 2024 23:56:14 +0300 Subject: [PATCH 1/6] =?UTF-8?q?feat(ui):=20highlight=20the=20currently=20s?= =?UTF-8?q?elected=20table=20from=20sidebar=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/core/package.json | 1 + apps/core/src/routes/dashboard/_layout.tsx | 15 ++++++++++----- bun.lockb | Bin 610120 -> 610912 bytes 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/core/package.json b/apps/core/package.json index 04a5f2e..a6dd26d 100644 --- a/apps/core/package.json +++ b/apps/core/package.json @@ -57,6 +57,7 @@ "react-resizable-panels": "^2.1.1", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.0", "zod": "^3.23.8", "zustand": "^4.5.5" }, diff --git a/apps/core/src/routes/dashboard/_layout.tsx b/apps/core/src/routes/dashboard/_layout.tsx index 37673b0..9373763 100644 --- a/apps/core/src/routes/dashboard/_layout.tsx +++ b/apps/core/src/routes/dashboard/_layout.tsx @@ -49,6 +49,7 @@ export const Route = createFileRoute("/dashboard/_layout")({ function DashboardLayout() { const deps = Route.useLoaderDeps() const data = Route.useLoaderData() + const searchParams = Route.useSearch() const keybindingsManager = useKeybindings() const [, setSideBarCollapsed] = useState(false) // NOTE: I don't know why this is needed, but collapsing doesn't work without it. const [tables, setTables] = useState(data!.tables) @@ -130,8 +131,8 @@ function DashboardLayout() { className="h-6 border-none text-sm transition-all placeholder:text-xs focus-visible:ring-0 focus-visible:ring-offset-0 lg:h-8 lg:w-[170px] lg:placeholder:text-base lg:focus:w-full" /> -
-
    +
    +
      {tables.map((table, index) => { return ( - - {table} +
      +

      {table}

      ) })} diff --git a/bun.lockb b/bun.lockb index 33038a2e0a536659b533d2671c17087479b0965d..0e596f492542927c73bf654bb7f2d0eb77fd637f 100644 GIT binary patch delta 26116 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 z delta 25766 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.C@Bqp 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 Date: Mon, 16 Sep 2024 00:01:00 +0300 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20set=20active=20table=20in=20local?= =?UTF-8?q?=20storage=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/core/src/routes/dashboard/_layout.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/core/src/routes/dashboard/_layout.tsx b/apps/core/src/routes/dashboard/_layout.tsx index 9373763..a042466 100644 --- a/apps/core/src/routes/dashboard/_layout.tsx +++ b/apps/core/src/routes/dashboard/_layout.tsx @@ -18,6 +18,7 @@ import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router" import { ArrowLeft, PanelLeftClose, Table } from "lucide-react" import { useEffect, useRef, useState, type KeyboardEvent } from "react" import type { ImperativePanelHandle } from "react-resizable-panels" +import { useLocalStorage } from "usehooks-ts" import { z } from "zod" const dashboardConnectionSchema = z.object({ @@ -54,6 +55,7 @@ function DashboardLayout() { const [, setSideBarCollapsed] = useState(false) // NOTE: I don't know why this is needed, but collapsing doesn't work without it. const [tables, setTables] = useState(data!.tables) const sidebarPanelRef = useRef(null) + const [, setActiveTable] = useLocalStorage("@tablex/active-table", "") useEffect(() => { keybindingsManager.registerKeybindings([ @@ -136,6 +138,7 @@ function DashboardLayout() { {tables.map((table, index) => { return ( setActiveTable(table)} to="/dashboard/$tableName" params={{ tableName: table From f294a7522c8197572ddf5709efad57e5d28301a6 Mon Sep 17 00:00:00 2001 From: Kareem Ebrahim Date: Mon, 16 Sep 2024 00:17:08 +0300 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20use=20active=20table=20stored=20in?= =?UTF-8?q?=20local=20storage=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/core/src/routes/connections/route.tsx | 16 ++++++++++++++++ apps/core/src/routes/dashboard/_layout.tsx | 5 +---- .../dashboard/_layout/$tableName/route.tsx | 10 +++++++++- apps/core/src/types/index.ts | 4 ++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 apps/core/src/types/index.ts diff --git a/apps/core/src/routes/connections/route.tsx b/apps/core/src/routes/connections/route.tsx index ff991ba..5e2b70e 100644 --- a/apps/core/src/routes/connections/route.tsx +++ b/apps/core/src/routes/connections/route.tsx @@ -10,9 +10,11 @@ import { } from "@/components/ui/dropdown-menu" import { Separator } from "@/components/ui/separator" import { unwrapResult } from "@/lib/utils" +import { ActiveTableLocalStorage } from "@/types" import { createFileRoute, redirect, useRouter } from "@tanstack/react-router" import { MoreHorizontal, Trash } from "lucide-react" import { Suspense } from "react" +import { useLocalStorage } from "usehooks-ts" export const Route = createFileRoute("/connections")({ beforeLoad: async () => { @@ -38,6 +40,10 @@ export const Route = createFileRoute("/connections")({ function ConnectionsPage() { const router = useRouter() const connections = Route.useLoaderData() + const [activeTable] = useLocalStorage( + "@tablex/active-table", + null + ) const onClickConnect = async (connectionId: string) => { const connectionDetailsResult = @@ -54,6 +60,16 @@ function ConnectionsPage() { if (connectionEstablishment === false) return + if (activeTable?.connectionName == connectionDetails.connName) { + return router.navigate({ + to: "/dashboard/$tableName", + params: { + tableName: activeTable.tableName + }, + search: { connectionName: connectionDetails.connName } + }) + } + router.navigate({ to: "/dashboard/land", search: { connectionName: connectionDetails.connName } diff --git a/apps/core/src/routes/dashboard/_layout.tsx b/apps/core/src/routes/dashboard/_layout.tsx index a042466..27fef83 100644 --- a/apps/core/src/routes/dashboard/_layout.tsx +++ b/apps/core/src/routes/dashboard/_layout.tsx @@ -18,11 +18,10 @@ import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router" import { ArrowLeft, PanelLeftClose, Table } from "lucide-react" import { useEffect, useRef, useState, type KeyboardEvent } from "react" import type { ImperativePanelHandle } from "react-resizable-panels" -import { useLocalStorage } from "usehooks-ts" import { z } from "zod" const dashboardConnectionSchema = z.object({ - connectionName: z.string().optional(), + connectionName: z.string(), tableName: z.string().optional() }) @@ -55,7 +54,6 @@ function DashboardLayout() { const [, setSideBarCollapsed] = useState(false) // NOTE: I don't know why this is needed, but collapsing doesn't work without it. const [tables, setTables] = useState(data!.tables) const sidebarPanelRef = useRef(null) - const [, setActiveTable] = useLocalStorage("@tablex/active-table", "") useEffect(() => { keybindingsManager.registerKeybindings([ @@ -138,7 +136,6 @@ function DashboardLayout() { {tables.map((table, index) => { return ( setActiveTable(table)} to="/dashboard/$tableName" params={{ tableName: table diff --git a/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx b/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx index 2067f2a..22e88db 100644 --- a/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx +++ b/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx @@ -1,15 +1,17 @@ import LoadingSpinner from "@/components/loading-spinner" import { useGetTableColumns } from "@/hooks/table" import { useTableState } from "@/state/tableState" +import type { ActiveTableLocalStorage } from "@/types" import { createFileRoute } from "@tanstack/react-router" import { useEffect } from "react" import { toast } from "react-hot-toast" +import { useLocalStorage } from "usehooks-ts" import { z } from "zod" import DataTable from "./-components/data-table" const tablePageSchema = z.object({ tableName: z.string().optional(), - connectionName: z.string().optional() + connectionName: z.string() }) export const Route = createFileRoute("/dashboard/_layout/$tableName")({ @@ -19,9 +21,15 @@ export const Route = createFileRoute("/dashboard/_layout/$tableName")({ function TableData() { const { tableName } = Route.useParams() + const { connectionName } = Route.useSearch() const { updateTableName } = useTableState() + const [, setActiveTable] = useLocalStorage( + "@tablex/active-table", + null + ) useEffect(() => updateTableName(tableName), [tableName, updateTableName]) + useEffect(() => setActiveTable({ connectionName, tableName })) const { data: columns, diff --git a/apps/core/src/types/index.ts b/apps/core/src/types/index.ts new file mode 100644 index 0000000..a8160d2 --- /dev/null +++ b/apps/core/src/types/index.ts @@ -0,0 +1,4 @@ +export type ActiveTableLocalStorage = { + connectionName: string + tableName: string +} From e776027f1fce9968ca6417d3457099060d173bde Mon Sep 17 00:00:00 2001 From: Kareem Ebrahim Date: Mon, 16 Sep 2024 11:02:11 +0300 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20use=20auto=20save=20Ids=20for=20per?= =?UTF-8?q?sisting=20panels=20position=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/core/src/routes/dashboard/_layout.tsx | 40 ++++++++++--------- .../$tableName/-components/data-table.tsx | 2 +- .../$tableName/-components/table-actions.tsx | 9 +++-- .../dashboard/_layout/$tableName/route.tsx | 2 +- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/apps/core/src/routes/dashboard/_layout.tsx b/apps/core/src/routes/dashboard/_layout.tsx index 27fef83..5f86d00 100644 --- a/apps/core/src/routes/dashboard/_layout.tsx +++ b/apps/core/src/routes/dashboard/_layout.tsx @@ -16,8 +16,9 @@ import { unwrapResult } from "@/lib/utils" import { cn } from "@tablex/lib/utils" import { createFileRoute, Link, Outlet, redirect } from "@tanstack/react-router" import { ArrowLeft, PanelLeftClose, Table } from "lucide-react" -import { useEffect, useRef, useState, type KeyboardEvent } from "react" +import { useEffect, useRef, useState } from "react" import type { ImperativePanelHandle } from "react-resizable-panels" +import { useDebounceCallback } from "usehooks-ts" import { z } from "zod" const dashboardConnectionSchema = z.object({ @@ -51,8 +52,8 @@ function DashboardLayout() { const data = Route.useLoaderData() const searchParams = Route.useSearch() const keybindingsManager = useKeybindings() - const [, setSideBarCollapsed] = useState(false) // NOTE: I don't know why this is needed, but collapsing doesn't work without it. - const [tables, setTables] = useState(data!.tables) + const [tables, setTables] = useState(data.tables) + const [, setSideBarCollapsed] = useState(false) const sidebarPanelRef = useRef(null) useEffect(() => { @@ -64,30 +65,31 @@ function DashboardLayout() { ]) }, [keybindingsManager]) - let timeout: NodeJS.Timeout - const handleKeyUp = (e: KeyboardEvent) => { - const searchPattern = e.currentTarget.value + const handleTableSearch = (searchPattern: string) => { if (searchPattern === "") return setTables(data!.tables) - clearTimeout(timeout) - - timeout = setTimeout(() => { - const filteredTables = tables.filter((table) => - table.includes(searchPattern) - ) - setTables(filteredTables) - }, 100) + const filteredTables = tables.filter((table) => + table.includes(searchPattern) + ) + setTables(filteredTables) } + const debounced = useDebounceCallback(handleTableSearch) + return ( - + setSideBarCollapsed(true)} onExpand={() => setSideBarCollapsed(false)} + onCollapse={() => setSideBarCollapsed(true)} collapsible - minSize={0} + minSize={12} + collapsedSize={0} className={cn( "flex flex-col items-start justify-between bg-zinc-800 p-4 pt-2 transition-all lg:p-6", sidebarPanelRef.current?.isCollapsed() && "w-0 p-0 lg:w-0 lg:p-0" @@ -126,7 +128,7 @@ function DashboardLayout() {
      debounced(e.currentTarget.value)} placeholder="Search..." className="h-6 border-none text-sm transition-all placeholder:text-xs focus-visible:ring-0 focus-visible:ring-offset-0 lg:h-8 lg:w-[170px] lg:placeholder:text-base lg:focus:w-full" /> @@ -147,7 +149,7 @@ function DashboardLayout() { preload={false} key={index} className={cn( - "hover:bg-muted-foreground/30 flex w-full items-center gap-x-1 rounded-md p-1 text-sm text-white lg:text-base", + "hover:bg-muted-foreground/30 flex w-full items-center gap-x-1 overflow-x-hidden rounded-md p-1 text-sm text-white lg:text-base", searchParams.tableName === table && "bg-muted-foreground/30" )} diff --git a/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx b/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx index 5f9135e..6beb0d6 100644 --- a/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx +++ b/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx @@ -83,7 +83,7 @@ const DataTable = ({ columns }: DataTableProps) => { const virtualizer = useVirtualizer({ count: table.getRowCount(), getScrollElement: () => parentRef.current, - estimateSize: () => 50, // I reached to this number by trial and error + estimateSize: () => 34, // I reached to this number by trial and error overscan: 10, debug: import.meta.env.DEV }) diff --git a/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx b/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx index 8463a45..f25ec2f 100644 --- a/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx +++ b/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx @@ -21,6 +21,7 @@ import { } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { useTableState } from "@/state/tableState" +import { useDebounceCallback } from "usehooks-ts" type TableActionsProps = { table: Table @@ -29,6 +30,10 @@ type TableActionsProps = { const TableActions = ({ table }: TableActionsProps) => { const { toggleDialog: toggleSqlEditor } = useSqlEditorState() const { tableName, setGlobalFilter } = useTableState() + const debounced = useDebounceCallback( + (filter: string) => setGlobalFilter(filter), + 200 + ) return ( <>
      @@ -39,9 +44,7 @@ const TableActions = ({ table }: TableActionsProps) => { - setGlobalFilter(String(value.currentTarget.value)) - } + onChange={(value) => debounced(String(value.currentTarget.value))} />
      diff --git a/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx b/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx index 22e88db..50107a8 100644 --- a/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx +++ b/apps/core/src/routes/dashboard/_layout/$tableName/route.tsx @@ -43,7 +43,7 @@ function TableData() { if (isError) return toast.error(error.message, { id: "get_table_columns" }) return ( -
      +
      ) From bbd6a47c91b509632374ca79b2cdc03cdc610c85 Mon Sep 17 00:00:00 2001 From: Kareem Ebrahim Date: Mon, 16 Sep 2024 14:30:02 +0300 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20store=20current=20table=20data=20in?= =?UTF-8?q?=20local=20storage=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/core/src/hooks/table.ts | 17 ++++-- apps/core/src/routes/connections/route.tsx | 22 ++++---- apps/core/src/routes/dashboard/_layout.tsx | 17 +++--- .../$tableName/-components/data-table.tsx | 7 +-- .../$tableName/-components/table-actions.tsx | 52 +++++++++++++++---- .../dashboard/_layout/$tableName/route.tsx | 18 ++++--- apps/core/src/types/index.ts | 4 +- 7 files changed, 95 insertions(+), 42 deletions(-) diff --git a/apps/core/src/hooks/table.ts b/apps/core/src/hooks/table.ts index 8176806..2aa5ff2 100644 --- a/apps/core/src/hooks/table.ts +++ b/apps/core/src/hooks/table.ts @@ -2,6 +2,7 @@ import { getZodSchemaFromCols } from "@/commands/columns" import { generateColumnsDefs } from "@/routes/dashboard/_layout/$tableName/-components/columns" import { useSettings } from "@/settings/manager" import { useTableState } from "@/state/tableState" +import { TableLocalStorage } from "@/types" import { rankItem, type RankingInfo } from "@tanstack/match-sorter-utils" import { useQuery } from "@tanstack/react-query" import { @@ -16,6 +17,7 @@ import { type SortingState } from "@tanstack/react-table" import { useMemo, useRef, useState } from "react" +import { useLocalStorage } from "usehooks-ts" import { useGetPaginatedRows } from "./row" export const useGetTableColumns = (tableName: string) => { @@ -60,6 +62,7 @@ const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { type SetupReactTableOptions = { columns: ColumnDef[] tableName: string + connectionId: string } /** @@ -68,10 +71,11 @@ type SetupReactTableOptions = { */ export const useSetupReactTable = ({ tableName, - columns + columns, + connectionId }: SetupReactTableOptions) => { const { defaultData, pagination, setPagination, pageIndex, pageSize } = - useSetupPagination() + useSetupPagination(connectionId) const { globalFilter, setGlobalFilter } = useTableState() const { data: rows, isLoading: isRowsLoading } = useGetPaginatedRows( @@ -121,10 +125,15 @@ export const useSetupReactTable = ({ * Sets up the state and memoization for page index & page size * to be used in paginating the rows. */ -const useSetupPagination = () => { +const useSetupPagination = (connectionId: string) => { const settings = useSettings() + const [{ pageIndex: persistedPageIndex }] = + useLocalStorage(`@tablex/${connectionId}`, { + tableName: "", + pageIndex: 0 + }) const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, + pageIndex: persistedPageIndex, pageSize: settings.pageSize }) const defaultData = useMemo(() => [], []) diff --git a/apps/core/src/routes/connections/route.tsx b/apps/core/src/routes/connections/route.tsx index 5e2b70e..f485623 100644 --- a/apps/core/src/routes/connections/route.tsx +++ b/apps/core/src/routes/connections/route.tsx @@ -10,11 +10,10 @@ import { } from "@/components/ui/dropdown-menu" import { Separator } from "@/components/ui/separator" import { unwrapResult } from "@/lib/utils" -import { ActiveTableLocalStorage } from "@/types" +import { TableLocalStorage } from "@/types" import { createFileRoute, redirect, useRouter } from "@tanstack/react-router" import { MoreHorizontal, Trash } from "lucide-react" import { Suspense } from "react" -import { useLocalStorage } from "usehooks-ts" export const Route = createFileRoute("/connections")({ beforeLoad: async () => { @@ -40,10 +39,6 @@ export const Route = createFileRoute("/connections")({ function ConnectionsPage() { const router = useRouter() const connections = Route.useLoaderData() - const [activeTable] = useLocalStorage( - "@tablex/active-table", - null - ) const onClickConnect = async (connectionId: string) => { const connectionDetailsResult = @@ -60,19 +55,26 @@ function ConnectionsPage() { if (connectionEstablishment === false) return - if (activeTable?.connectionName == connectionDetails.connName) { + const connectionStorageData = localStorage.getItem( + `@tablex/${connectionId}` + ) + + if (connectionStorageData) { + const parsedConnectionData: TableLocalStorage = JSON.parse( + connectionStorageData + ) return router.navigate({ to: "/dashboard/$tableName", params: { - tableName: activeTable.tableName + tableName: parsedConnectionData.tableName }, - search: { connectionName: connectionDetails.connName } + search: { connectionId } }) } router.navigate({ to: "/dashboard/land", - search: { connectionName: connectionDetails.connName } + search: { connectionId } }) } diff --git a/apps/core/src/routes/dashboard/_layout.tsx b/apps/core/src/routes/dashboard/_layout.tsx index 5f86d00..b9a3625 100644 --- a/apps/core/src/routes/dashboard/_layout.tsx +++ b/apps/core/src/routes/dashboard/_layout.tsx @@ -22,18 +22,23 @@ import { useDebounceCallback } from "usehooks-ts" import { z } from "zod" const dashboardConnectionSchema = z.object({ - connectionName: z.string(), + connectionId: z.string().uuid(), tableName: z.string().optional() }) export const Route = createFileRoute("/dashboard/_layout")({ validateSearch: dashboardConnectionSchema, - loaderDeps: ({ search: { tableName, connectionName } }) => ({ - connectionName, + loaderDeps: ({ search: { tableName, connectionId } }) => ({ + connectionId, tableName }), - loader: async ({ deps: { connectionName } }) => { - const connName = connectionName || "Temp Connection" + loader: async ({ deps: { connectionId } }) => { + const connectionDetailsResult = + await commands.getConnectionDetails(connectionId) + const connectionDetails = unwrapResult(connectionDetailsResult) + + if (!connectionDetails) throw redirect({ to: "/connections" }) + const connName = connectionDetails.connName || "Temp Connection" const tables = unwrapResult(await commands.getTables()) if (!tables) @@ -143,7 +148,7 @@ function DashboardLayout() { tableName: table }} search={{ - connectionName: deps.connectionName, + connectionId: deps.connectionId, tableName: table }} preload={false} diff --git a/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx b/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx index 6beb0d6..18d48cb 100644 --- a/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx +++ b/apps/core/src/routes/dashboard/_layout/$tableName/-components/data-table.tsx @@ -40,12 +40,13 @@ import TableActions from "./table-actions" interface DataTableProps { columns: ColumnDef[] + connectionId: string } -const DataTable = ({ columns }: DataTableProps) => { +const DataTable = ({ columns, connectionId }: DataTableProps) => { const { tableName, pkColumn } = useTableState() const { isRowsLoading, contextMenuRow, setContextMenuRow, table, tableRef } = - useSetupReactTable({ columns, tableName }) + useSetupReactTable({ columns, tableName, connectionId }) const queryClient = useQueryClient() const keybindingsManager = useKeybindings() const { isOpen, toggleSheet } = useEditRowSheetState() @@ -90,7 +91,7 @@ const DataTable = ({ columns }: DataTableProps) => { return ( - + {isRowsLoading ? ( ) : ( diff --git a/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx b/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx index f25ec2f..53dbdf2 100644 --- a/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx +++ b/apps/core/src/routes/dashboard/_layout/$tableName/-components/table-actions.tsx @@ -21,18 +21,20 @@ import { } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { useTableState } from "@/state/tableState" -import { useDebounceCallback } from "usehooks-ts" +import { TableLocalStorage } from "@/types" +import { useDebounceCallback, useLocalStorage } from "usehooks-ts" type TableActionsProps = { table: Table + connectionId: string } -const TableActions = ({ table }: TableActionsProps) => { +const TableActions = ({ table, connectionId }: TableActionsProps) => { const { toggleDialog: toggleSqlEditor } = useSqlEditorState() const { tableName, setGlobalFilter } = useTableState() const debounced = useDebounceCallback( (filter: string) => setGlobalFilter(filter), - 200 + 500 ) return ( <> @@ -48,7 +50,7 @@ const TableActions = ({ table }: TableActionsProps) => { />
      - +