From 706f35d0420a6ecf0797582d5fa27dba0f17a554 Mon Sep 17 00:00:00 2001 From: Avisek Date: Mon, 4 May 2026 14:19:34 +0530 Subject: [PATCH 1/8] refactor rows --- .../src/feature/tables/components/Rows.tsx | 358 ++++++++---------- .../tables/components/row-details-sheet.tsx | 96 +++++ frontend/src/hooks/useRows.tsx | 42 ++ frontend/src/pages/TableRows.tsx | 51 +-- 4 files changed, 298 insertions(+), 249 deletions(-) create mode 100644 frontend/src/feature/tables/components/row-details-sheet.tsx create mode 100644 frontend/src/hooks/useRows.tsx diff --git a/frontend/src/feature/tables/components/Rows.tsx b/frontend/src/feature/tables/components/Rows.tsx index 06ea37b..1b9c05a 100644 --- a/frontend/src/feature/tables/components/Rows.tsx +++ b/frontend/src/feature/tables/components/Rows.tsx @@ -1,221 +1,179 @@ -import React from "react"; -import { Link } from "react-router-dom"; -import { MoreHorizontal } from "lucide-react"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; -import { cn } from "@/lib/utils"; +import React, { useState } from "react"; import type { ListRowsResponse } from "@/client"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + type ColumnDef, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { RowDetailsSheet } from "@/feature/tables/components/row-details-sheet"; +import { Input } from "@/components/ui/input"; +import { ChevronRight } from "lucide-react"; +import { useIsMobile } from "@/hooks/use-mobile"; interface RowsProps { + tableName: string; + isLoading: boolean; data: ListRowsResponse; - selectedRows: Record; - isAllSelected: boolean; - isSomeSelected: boolean; - toggleAllSelection: () => void; - toggleRowSelection: (index: number) => void; deleteRow: (hash: string) => void; } -export const Rows = ({ - data, - selectedRows, - isAllSelected, - isSomeSelected, - toggleAllSelection, - toggleRowSelection, - deleteRow, -}: RowsProps) => { - if (!data) return ; - return ( -
-
"minmax(150px, 1fr)").join(" ")} 80px`, - }} - > - {/* Checkbox Header */} -
- -
- - {/* Data Columns Headers */} - {data.cols?.map((col) => ( -
- {col.columnName} -
- ))} +export type RowData = { hash: string } & Record; - {/* Actions Header */} -
- Actions -
+export const Rows = ({ tableName, isLoading, data, deleteRow }: RowsProps) => { + const isMobile = useIsMobile(); + const [sheetData, setSheetData] = React.useState<{ + row: RowData; + tableName: string; + } | null>(null); + const [sheetOpen, setSheetOpen] = React.useState(false); + const [globalFilter, setGlobalFilter] = useState(""); - {/* Body Rows */} - {data?.rows && data?.rows?.length > 0 ? ( - data.rows?.map((row, rowIndex) => { - const hash = row.hash; - const isSelected = !!selectedRows[rowIndex]; + const columns: ColumnDef[] = React.useMemo( + () => [ + ...(data.cols?.map((col) => ({ + header: col.columnName, + accessorKey: col.columnName, + size: 200, + maxSize: 200, + })) || []), - return ( - - {/* Checkbox Cell */} -
- toggleRowSelection(rowIndex)} - aria-label="Select row" - /> -
- - {row.columns?.map((cell, cellIndex) => ( -
- {cell.value === null || cell.value === undefined - ? "NULL" - : String(cell.value)} -
- ))} + { + id: "action", + size: 0, + cell: () => ( + + ), + }, + ], + [data.cols], + ); - {/* Actions Cell */} -
- - - - - - Actions - - - Edit Row - - - - - - -
-
- ); - }) - ) : ( -
- No results. -
- )} -
-
+ const flattenedRows = React.useMemo( + () => + data.rows?.map((row) => ({ + hash: row.hash, + ...Object.fromEntries( + (row.columns || []).map((col) => [col.columnName, col.value]), + ), + })) || [], + [data.rows], ); -}; -interface RowsSkeletonProps { - columns?: number; - rows?: number; -} + const handleRowClick = (row: RowData) => { + setSheetData({ row: row, tableName }); + setSheetOpen(true); + }; + console.log({ data }); + const table = useReactTable({ + data: flattenedRows, + columns: columns, + state: { + globalFilter, + columnPinning: { + right: ["action"], + }, + }, + enableColumnPinning: true, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + }); -export const RowsSkeleton = ({ columns = 5, rows = 10 }: RowsSkeletonProps) => { return ( -
-
- {/* Checkbox Header Skeleton */} -
- -
- - {/* Column Headers Skeleton */} - {Array.from({ length: columns }).map((_, index) => ( -
- -
- ))} +
+ setGlobalFilter(e.target.value)} + className="max-w-sm" + /> - {/* Actions Header Skeleton */} -
- -
- - {/* Body Rows Skeleton */} - {Array.from({ length: rows }).map((_, rowIndex) => ( - - {/* Checkbox Cell Skeleton */} -
- -
- - {/* Data Cells Skeleton */} - {Array.from({ length: columns }).map((_, cellIndex) => ( -
+ + click the row to view complete data + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {isLoading ? ( + + + Loading... + + + ) : ( + table.getRowModel().rows.map((row) => ( + handleRowClick(row.original)} + key={row.id} + className="group cursor-pointer border-l-3 border-b-0 hover:border-primary" > - - - ))} + {row.getVisibleCells().map((cell) => { + const isPinned = cell.column.getIsPinned(); + return ( + - - - - ))} - + maxWidth: `${cell.column.getSize()}px`, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + )) + )} + +
); }; diff --git a/frontend/src/feature/tables/components/row-details-sheet.tsx b/frontend/src/feature/tables/components/row-details-sheet.tsx new file mode 100644 index 0000000..6408679 --- /dev/null +++ b/frontend/src/feature/tables/components/row-details-sheet.tsx @@ -0,0 +1,96 @@ +import { Button } from "@/components/ui/button"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import type { RowData } from "@/feature/tables/components/Rows"; +import { Link } from "react-router-dom"; + +interface RowDataSheetProps { + data: { row: RowData; tableName: string } | null; + setOpenChange: React.Dispatch>; + open: boolean; + deleteRow: (hash: string) => void; +} +const formatValue = (value: unknown) => { + if (typeof value === "string") { + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } + } + return value?.toString(); +}; +export const RowDetailsSheet = ({ + data, + setOpenChange, + open, + deleteRow, +}: RowDataSheetProps) => { + return ( + + + + {data?.tableName} + + Viewing{" "} + + {data?.tableName} + {" "} + details. + + + + +
+ {Object.entries(data?.row || {}).map(([key, value]) => ( +
+ +

+ {formatValue(value)} +

+
+ ))} +
+
+ + +
+ {data?.row.hash && ( + <> + + + + + )} +
+ + + +
+
+
+ ); +}; diff --git a/frontend/src/hooks/useRows.tsx b/frontend/src/hooks/useRows.tsx new file mode 100644 index 0000000..67abf35 --- /dev/null +++ b/frontend/src/hooks/useRows.tsx @@ -0,0 +1,42 @@ +import { + createContext, + type ReactNode, + useState, + type Dispatch, + type SetStateAction, +} from "react"; + +export type RowData = { hash: string } & Record; + +type SheetData = { row: RowData; tableName: string }; + +export interface RowContextType { + sheetData: SheetData | null; + setSheetData: Dispatch>; + sheetOpen: boolean; + setSheetOpen: Dispatch>; + globalFilter: string; + setGlobalFilter: Dispatch>; +} + +const RowContext = createContext(null); + +export const RowProvider = ({ children }: { children: ReactNode }) => { + const [sheetData, setSheetData] = useState(null); + const [sheetOpen, setSheetOpen] = useState(false); + const [globalFilter, setGlobalFilter] = useState(""); + return ( + + {children} + + ); +}; diff --git a/frontend/src/pages/TableRows.tsx b/frontend/src/pages/TableRows.tsx index b0005e6..9103927 100644 --- a/frontend/src/pages/TableRows.tsx +++ b/frontend/src/pages/TableRows.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { useParams, Link, useSearchParams } from "react-router-dom"; import { Plus } from "lucide-react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; @@ -17,11 +16,9 @@ export function TablePage() { const { tableName } = useParams<{ tableName: string }>(); const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); - const [selectedRows, setSelectedRows] = useState>({}); const page = parseInt(searchParams.get("page") || "1"); const col = searchParams.get("col"); const order = searchParams.get("order")?.toUpperCase() as "ASC" | "DESC"; - const { data, isLoading, error } = useQuery( listRowsOptions({ path: { tableName: tableName! }, @@ -54,38 +51,6 @@ export function TablePage() { }); }; - // Reset search params when table name changes - // useEffect(() => { - // setSearchParams({}, { replace: true }); - // setSelectedRows({}); - // }, [tableName, setSearchParams]); - - const toggleRowSelection = (index: number) => { - setSelectedRows((prev) => ({ - ...prev, - [index]: !prev[index], - })); - }; - - const toggleAllSelection = () => { - if (!data?.rows) return; - const allSelected = - data.rows.length > 0 && data.rows.every((_, idx) => selectedRows[idx]); - if (allSelected) { - setSelectedRows({}); - } else { - const newSelection: Record = {}; - data.rows.forEach((_, idx) => { - newSelection[idx] = true; - }); - setSelectedRows(newSelection); - } - }; - - if (isLoading) { - return
Loading...
; - } - if (error) { return (
@@ -100,11 +65,6 @@ export function TablePage() { return
No data found.
; } - const isAllSelected = - data.rows.length > 0 && data.rows.every((_, idx) => selectedRows[idx]); - const isSomeSelected = - data.rows.some((_, idx) => selectedRows[idx]) && !isAllSelected; - return (
@@ -133,19 +93,12 @@ export function TablePage() {
-
- {Object.values(selectedRows).filter(Boolean).length} of{" "} - {data.rows?.length || 0} row(s) selected. -
Date: Mon, 4 May 2026 21:36:14 +0530 Subject: [PATCH 2/8] feat: implement row insertion and update functionality with dedicated form component --- .../rows/row-insert-or-update-form.tsx | 20 +++++ .../src/feature/tables/components/Rows.tsx | 29 ++----- .../tables/components/row-details-sheet.tsx | 50 ++++++------ frontend/src/hooks/useRows.tsx | 80 ++++++++++++++++++- frontend/src/pages/TableRows.tsx | 74 +++++------------ internal/database/queries/dialects_impl.go | 2 + internal/database/repo/quires.sql.go | 39 +++------ internal/router/handler.go | 2 - 8 files changed, 165 insertions(+), 131 deletions(-) create mode 100644 frontend/src/feature/rows/row-insert-or-update-form.tsx diff --git a/frontend/src/feature/rows/row-insert-or-update-form.tsx b/frontend/src/feature/rows/row-insert-or-update-form.tsx new file mode 100644 index 0000000..e1509e5 --- /dev/null +++ b/frontend/src/feature/rows/row-insert-or-update-form.tsx @@ -0,0 +1,20 @@ +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; + +export const IowInsertOrUpdateForm = () => { + return ( + + + + {"Sheet Title"} + Viewing record details. + + + + ); +}; diff --git a/frontend/src/feature/tables/components/Rows.tsx b/frontend/src/feature/tables/components/Rows.tsx index 1b9c05a..6eb060e 100644 --- a/frontend/src/feature/tables/components/Rows.tsx +++ b/frontend/src/feature/tables/components/Rows.tsx @@ -1,5 +1,4 @@ import React, { useState } from "react"; -import type { ListRowsResponse } from "@/client"; import { flexRender, getCoreRowModel, @@ -23,28 +22,22 @@ import { RowDetailsSheet } from "@/feature/tables/components/row-details-sheet"; import { Input } from "@/components/ui/input"; import { ChevronRight } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; +import { useRowContext } from "@/hooks/useRows"; interface RowsProps { tableName: string; - isLoading: boolean; - data: ListRowsResponse; - deleteRow: (hash: string) => void; } export type RowData = { hash: string } & Record; -export const Rows = ({ tableName, isLoading, data, deleteRow }: RowsProps) => { +export const Rows = ({ tableName }: RowsProps) => { const isMobile = useIsMobile(); - const [sheetData, setSheetData] = React.useState<{ - row: RowData; - tableName: string; - } | null>(null); - const [sheetOpen, setSheetOpen] = React.useState(false); + const { setSheetOpen, setSheetData, isLoading, data } = useRowContext(); const [globalFilter, setGlobalFilter] = useState(""); const columns: ColumnDef[] = React.useMemo( () => [ - ...(data.cols?.map((col) => ({ + ...(data?.cols?.map((col) => ({ header: col.columnName, accessorKey: col.columnName, size: 200, @@ -59,25 +52,24 @@ export const Rows = ({ tableName, isLoading, data, deleteRow }: RowsProps) => { ), }, ], - [data.cols], + [data?.cols], ); const flattenedRows = React.useMemo( () => - data.rows?.map((row) => ({ + data?.rows?.map((row) => ({ hash: row.hash, ...Object.fromEntries( (row.columns || []).map((col) => [col.columnName, col.value]), ), })) || [], - [data.rows], + [data?.rows], ); const handleRowClick = (row: RowData) => { setSheetData({ row: row, tableName }); setSheetOpen(true); }; - console.log({ data }); const table = useReactTable({ data: flattenedRows, columns: columns, @@ -103,12 +95,7 @@ export const Rows = ({ tableName, isLoading, data, deleteRow }: RowsProps) => { className="max-w-sm" /> - + click the row to view complete data diff --git a/frontend/src/feature/tables/components/row-details-sheet.tsx b/frontend/src/feature/tables/components/row-details-sheet.tsx index 6408679..326ed59 100644 --- a/frontend/src/feature/tables/components/row-details-sheet.tsx +++ b/frontend/src/feature/tables/components/row-details-sheet.tsx @@ -9,16 +9,11 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; -import type { RowData } from "@/feature/tables/components/Rows"; +import { useRowContext } from "@/hooks/useRows"; import { Link } from "react-router-dom"; -interface RowDataSheetProps { - data: { row: RowData; tableName: string } | null; - setOpenChange: React.Dispatch>; - open: boolean; - deleteRow: (hash: string) => void; -} const formatValue = (value: unknown) => { + console.log({ value }); if (typeof value === "string") { try { return JSON.stringify(JSON.parse(value), null, 2); @@ -28,35 +23,36 @@ const formatValue = (value: unknown) => { } return value?.toString(); }; -export const RowDetailsSheet = ({ - data, - setOpenChange, - open, - deleteRow, -}: RowDataSheetProps) => { +export const RowDetailsSheet = () => { + const { sheetData, setSheetOpen, sheetOpen, deleteRow } = useRowContext(); + console.log({ sheetData }); + return ( - + - {data?.tableName} - - Viewing{" "} - - {data?.tableName} - {" "} - details. - + {sheetData?.tableName} + Viewing record details.
- {Object.entries(data?.row || {}).map(([key, value]) => ( + {Object.entries(sheetData?.row || {}).map(([key, value]) => (

- {formatValue(value)} + {(() => { + switch (typeof value) { + case "undefined": + return

-

; + case null: + return

null

; + default: + return formatValue(value); + } + })()}

))} @@ -65,11 +61,11 @@ export const RowDetailsSheet = ({
- {data?.row.hash && ( + {sheetData?.row.hash && ( <> diff --git a/frontend/src/hooks/useRows.tsx b/frontend/src/hooks/useRows.tsx index 67abf35..e57fc29 100644 --- a/frontend/src/hooks/useRows.tsx +++ b/frontend/src/hooks/useRows.tsx @@ -2,9 +2,19 @@ import { createContext, type ReactNode, useState, + useContext, type Dispatch, type SetStateAction, } from "react"; +import type { ErrorModel, ListRowsResponse } from "@/client"; +import { + deleteRowMutation, + listRowsOptions, + listRowsQueryKey, +} from "@/client/@tanstack/react-query.gen"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { useSearchParams } from "react-router-dom"; export type RowData = { hash: string } & Record; @@ -17,14 +27,65 @@ export interface RowContextType { setSheetOpen: Dispatch>; globalFilter: string; setGlobalFilter: Dispatch>; + tableName: string; + isLoading: boolean; + deleteRow: (hash: string) => void; + data?: ListRowsResponse; + page: number; + rowFetchError: ErrorModel | null; } const RowContext = createContext(null); - -export const RowProvider = ({ children }: { children: ReactNode }) => { +interface RowProviderProps { + children: ReactNode; + page?: number; + tableName: string; +} +export const RowProvider = ({ children, tableName }: RowProviderProps) => { const [sheetData, setSheetData] = useState(null); const [sheetOpen, setSheetOpen] = useState(false); const [globalFilter, setGlobalFilter] = useState(""); + + const [searchParams] = useSearchParams(); + const page = parseInt(searchParams.get("page") || "1"); + const col = searchParams.get("col"); + const order = searchParams.get("order")?.toUpperCase() as "ASC" | "DESC"; + + const queryClient = useQueryClient(); + + const deleteMutation = useMutation({ + ...deleteRowMutation(), + onSuccess: () => { + toast.success("Row deleted successfully"); + queryClient.invalidateQueries({ + queryKey: listRowsQueryKey({ path: { tableName: tableName! } }), + }); + setSheetOpen(false); + setSheetData(null); + }, + }); + + const deleteRow = async (hash: string) => { + await deleteMutation.mutateAsync({ + path: { tableName: tableName!, hash }, + query: { page }, + }); + }; + + const { + data, + isLoading, + error: rowFetchError, + } = useQuery( + listRowsOptions({ + path: { tableName: tableName! }, + query: { + page, + column: col || undefined, + order: order || undefined, + }, + }), + ); return ( { globalFilter, setGlobalFilter, setSheetOpen, + tableName, + isLoading, + deleteRow, + data, + rowFetchError, + page, }} > {children} ); }; + +// eslint-disable-next-line react-refresh/only-export-components +export function useRowContext() { + const context = useContext(RowContext); + if (!context) { + throw new Error("useRowContext must be used within a RowProvider"); + } + return context; +} diff --git a/frontend/src/pages/TableRows.tsx b/frontend/src/pages/TableRows.tsx index 9103927..4b7c96d 100644 --- a/frontend/src/pages/TableRows.tsx +++ b/frontend/src/pages/TableRows.tsx @@ -1,61 +1,21 @@ import { useParams, Link, useSearchParams } from "react-router-dom"; import { Plus } from "lucide-react"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { - listRowsOptions, - deleteRowMutation, - listRowsQueryKey, -} from "@/client/@tanstack/react-query.gen"; import { Button } from "@/components/ui/button"; import { AppPagination } from "@/components/shared/AppPagination"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { toast } from "sonner"; import { DeleteAlert, RowOrderForm, Rows } from "@/feature/tables"; +import { RowProvider, useRowContext } from "@/hooks/useRows"; -export function TablePage() { +function TablePageContent() { const { tableName } = useParams<{ tableName: string }>(); const [searchParams, setSearchParams] = useSearchParams(); - const queryClient = useQueryClient(); - const page = parseInt(searchParams.get("page") || "1"); - const col = searchParams.get("col"); - const order = searchParams.get("order")?.toUpperCase() as "ASC" | "DESC"; - const { data, isLoading, error } = useQuery( - listRowsOptions({ - path: { tableName: tableName! }, - query: { - page, - column: col || undefined, - order: order || undefined, - }, - }), - ); - - const deleteMutation = useMutation({ - ...deleteRowMutation(), - onSuccess: () => { - toast.success("Row deleted successfully"); - queryClient.invalidateQueries({ - queryKey: listRowsQueryKey({ path: { tableName: tableName! } }), - }); - }, - onError: (err) => { - const errorMessage = err?.detail || "Failed to delete row"; - toast.error(errorMessage); - }, - }); + const { page, rowFetchError, data } = useRowContext(); - const deleteRow = async (hash: string) => { - await deleteMutation.mutateAsync({ - path: { tableName: tableName!, hash }, - query: { page }, - }); - }; - - if (error) { + if (rowFetchError) { return (
- Error: {error?.detail || "An unknown error occurred"} + Error: {rowFetchError?.detail || "An unknown error occurred"}
); @@ -87,17 +47,17 @@ export function TablePage() { Table Data - +
); } + +export function TablePage() { + const { tableName } = useParams<{ tableName: string }>(); + + return ( + + + + ); +} diff --git a/internal/database/queries/dialects_impl.go b/internal/database/queries/dialects_impl.go index 3367c52..9f61d60 100644 --- a/internal/database/queries/dialects_impl.go +++ b/internal/database/queries/dialects_impl.go @@ -60,6 +60,7 @@ func quoteName(name string, format ...string) string { func whereClause(d Dialect, cols []models.ColValue, argsIdx int) (string, []any, error) { var mixed []string var args []any + logger.Debug("whereClause: %+v", cols) for i, col := range cols { ph, err := d.PlaceHolder(argsIdx + i) if err != nil { @@ -95,6 +96,7 @@ func whereClause(d Dialect, cols []models.ColValue, argsIdx int) (string, []any, finalCLause = "WHERE " + finalCLause } + logger.Debug("WHERE CLAUSE %q", finalCLause) return finalCLause, args, nil } diff --git a/internal/database/repo/quires.sql.go b/internal/database/repo/quires.sql.go index 96d7ced..bc46edd 100644 --- a/internal/database/repo/quires.sql.go +++ b/internal/database/repo/quires.sql.go @@ -7,7 +7,6 @@ import ( "fmt" "github.com/biisal/rowsql/configs" - "github.com/biisal/rowsql/internal/apperr" "github.com/biisal/rowsql/internal/database/models" "github.com/biisal/rowsql/internal/logger" "github.com/biisal/rowsql/internal/utils" @@ -221,11 +220,9 @@ func (q *Queries) InsertRow(ctx context.Context, tableName string, form []models func (q *Queries) GetRow(ctx context.Context, tableName, hash string, offest, limit int) ([]models.ColValue, error) { if row := q.cache.Get(hash); row != nil { - logger.Info("found data in cache: %v", row) if cv, ok := row.([]models.ColValue); ok { return cv, nil } - return nil, apperr.ErrorNotSameRowColsSize } colValues, err := q.ListColsMetaData(ctx, tableName) if err != nil { @@ -268,19 +265,18 @@ func (q *Queries) GetRow(ctx context.Context, tableName, hash string, offest, li } func (q *Queries) DeleteRow(ctx context.Context, props UpdateOrDeleteRowProps) error { - // var rowValues []any - // if cached := q.cache.Get(props.Hash); cached != nil { - // rowValues = cached - // } else { - // row, err := q.GetRow(ctx, props.TableName, props.Hash, props.Offset, props.Limit) - // if err != nil { - // return err - // } - // rowValues = row.Values - - // return err - // } - query, args, err := q.queryBuilder.DeleteRow(props.TableName, props.Values, 1) + var rowValues []models.ColValue + if cached := q.cache.Get(props.Hash); cached != nil { + rowValues = cached.([]models.ColValue) + } else { + row, err := q.GetRow(ctx, props.TableName, props.Hash, props.Offset, props.Limit) + if err != nil { + return err + } + rowValues = row + } + + query, args, err := q.queryBuilder.DeleteRow(props.TableName, rowValues, 1) if err != nil { return err } @@ -306,17 +302,6 @@ type UpdateOrDeleteRowProps struct { } func (q *Queries) UpdateRow(ctx context.Context, props UpdateOrDeleteRowProps) error { - // var rowValues []any - // if cached := q.cache.Get(props.Hash); cached != nil { - // rowValues = cached - // } else { - // row, err := q.GetRow(ctx, props.TableName, props.Hash, props.Offset, props.Limit) - // if err != nil { - // return err - // } - // rowValues = row.Values - // } - query, args, err := q.queryBuilder.UpdateRow(props.TableName, props.Values) logger.Info("Query to Update : %s", query) if err != nil { diff --git a/internal/router/handler.go b/internal/router/handler.go index 8759c60..75bdafe 100644 --- a/internal/router/handler.go +++ b/internal/router/handler.go @@ -2,7 +2,6 @@ package router import ( "context" - "log/slog" "strings" "github.com/biisal/rowsql/internal/apperr" @@ -161,7 +160,6 @@ func (h DBHandler) RowInsertOrUpdateForm(ctx context.Context, input *RowInsertOr if err != nil { return nil, err } - slog.Info("Initial row", "data", initialRow) return &RowInsertOrUpdateFormOutput{ Body: struct { Cols []models.ColValue `json:"cols"` From 4cfe60eb319ebf34ed757e0134b528a25eb89d5f Mon Sep 17 00:00:00 2001 From: Avisek Date: Tue, 5 May 2026 09:47:42 +0530 Subject: [PATCH 3/8] feat: add Drawer component and migrate row insert/update form to a sheet-based UI --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 18 + frontend/src/components/ui/drawer.tsx | 133 +++++++ .../components/row-insert-or-update-form.tsx | 368 ++++++++++++++++++ .../rows/row-insert-or-update-form.tsx | 20 - .../tables/components/row-details-sheet.tsx | 14 +- frontend/src/pages/TableRows.tsx | 11 +- 7 files changed, 528 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/ui/drawer.tsx create mode 100644 frontend/src/feature/rows/components/row-insert-or-update-form.tsx delete mode 100644 frontend/src/feature/rows/row-insert-or-update-form.tsx diff --git a/frontend/package.json b/frontend/package.json index 32d0858..15239a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -43,6 +43,7 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.17", + "vaul": "^1.1.2", "zod": "^4.2.0", "zustand": "^5.0.9" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 8edf822..b9bf8c6 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: tailwindcss: specifier: ^4.1.17 version: 4.1.17 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) zod: specifier: ^4.2.0 version: 4.2.0 @@ -2749,6 +2752,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + vite@7.2.4: resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5357,6 +5366,15 @@ snapshots: dependencies: react: 19.2.0 + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3): dependencies: esbuild: 0.25.12 diff --git a/frontend/src/components/ui/drawer.tsx b/frontend/src/components/ui/drawer.tsx new file mode 100644 index 0000000..dc15fad --- /dev/null +++ b/frontend/src/components/ui/drawer.tsx @@ -0,0 +1,133 @@ +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; + +import { cn } from "@/lib/utils"; + +function Drawer({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return ; +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ); +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/frontend/src/feature/rows/components/row-insert-or-update-form.tsx b/frontend/src/feature/rows/components/row-insert-or-update-form.tsx new file mode 100644 index 0000000..67702ec --- /dev/null +++ b/frontend/src/feature/rows/components/row-insert-or-update-form.tsx @@ -0,0 +1,368 @@ +import { useEffect } from "react"; +import { useParams, useSearchParams, useNavigate } from "react-router-dom"; +import { Controller, useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Input } from "@/components/ui/input"; +import { toast } from "sonner"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + insertOrUpdateRowMutation, + rowInsertOrUpdateFormOptions, +} from "@/client/@tanstack/react-query.gen"; +import type { ColValue, ErrorModel } from "@/client"; +import { zColValue } from "@/client/zod.gen"; + +const zColField = zColValue.extend({ + size: z.coerce.number().optional(), + value: z.union([z.string(), z.boolean(), z.number(), z.null()]).default(null), + useDefault: z.boolean().default(false), + useAutoIncrement: z.boolean().default(false), +}); + +const formSchema = z.object({ + cols: z.array(zColField).min(1, "At least one column required"), +}); + +type FormInput = z.input; +type FormSchema = z.output; +type ColField = z.infer; + +const colFieldValueSchema = z + .union([z.string(), z.boolean(), z.number(), z.null()]) + .catch(null); + +function buildDefaultCols(cols: ColValue[]): ColField[] { + return cols.map((col) => ({ + columnName: col.columnName, + columnType: col.columnType, + defaultValue: col.defaultValue, + size: col.size !== undefined ? Number(col.size) : undefined, + value: colFieldValueSchema.parse(col.value), + useDefault: false, + useAutoIncrement: false, + })); +} +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; + +import { Button } from "@/components/ui/button"; +import { Loader2, Plus, Save } from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface RowInsertOrUpdateFormProps { + children?: React.ReactNode; + hash?: string; +} + +export const RowInsertOrUpdateForm = ({ + hash, + children, +}: RowInsertOrUpdateFormProps) => { + const { tableName } = useParams<{ tableName: string }>(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const page = Math.max(1, Number(searchParams.get("page")) || 1); + const isEdit = !!hash; + + const { data, isPending } = useQuery( + rowInsertOrUpdateFormOptions({ + path: { tableName: tableName || "" }, + query: { hash: hash || undefined, page }, + }), + ); + + const { mutateAsync: insertOrUpdateMutation } = useMutation( + insertOrUpdateRowMutation(), + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + cols: [], + }, + mode: "onChange", + }); + + const { fields, replace } = useFieldArray({ + control: form.control, + name: "cols", + }); + + useEffect(() => { + if (data?.cols) { + replace(buildDefaultCols(data.cols)); + } + }, [data, replace]); + + const onSubmit = async (formValues: FormSchema) => { + if (!tableName) return; + await insertOrUpdateMutation( + { + path: { + tableName: tableName, + }, + body: formValues.cols, + query: { + hash: hash || undefined, + page: page, + }, + }, + { + onError: (error: ErrorModel) => { + console.error( + "Mutation error:", + error.errors?.[0]?.message || error.detail, + ); + }, + onSuccess: () => { + toast.success("Row inserted/updated successfully"); + navigate(`/tables/${tableName}?page=${page}`); + }, + }, + ); + }; + return ( + + + {children || ( + + )} + + e.preventDefault()} + className="min-w-[90%] md:min-w-xl flex flex-col p-0" + > + {isPending ? ( +
+ +
+ ) : ( +
+ + + {isEdit ? "Update" : "Insert"} Row + + {isEdit + ? "Update the details of the existing record." + : "Fill in the fields below to add a new record to the table."} + + + + {form.formState.errors.cols?.message && ( +
+ {form.formState.errors.cols.message} +
+ )} + + +
+ {fields.length === 0 ? ( +
+ No columns found for this table. +
+ ) : ( + fields.map((field, index) => { + const { columnName, columnType } = field; + + return ( + { + const useDefault = form.watch( + `cols.${index}.useDefault`, + ); + const useAutoIncrement = form.watch( + `cols.${index}.useAutoIncrement`, + ); + const isDisabled = useDefault || useAutoIncrement; + + return ( + + + {columnName.replace(/_/g, " ")} + + ({columnType.dataType}) + + {columnType.isUnique && ( + + • Unique + + )} + + + {columnType.hasDefault && ( + ( + + )} + /> + )} + + {columnType.hasAutoIncrement && ( + ( + + )} + /> + )} + + {columnType.inputType === "checkbox" ? ( +
+ + inputField.onChange(c === true) + } + /> + + Enable {columnName.replace(/_/g, " ")} + +
+ ) : columnType.inputType === "textarea" || + columnType.inputType === "json" ? ( +