mirror of
https://github.com/ershisan99/db-studio.git
synced 2025-12-16 20:59:23 +00:00
wip: editable rows
This commit is contained in:
26
frontend/src/components/db-table-view/body-cell.tsx
Normal file
26
frontend/src/components/db-table-view/body-cell.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { ComponentPropsWithoutRef } from "react";
|
||||||
|
|
||||||
|
export function BodyCell({
|
||||||
|
value,
|
||||||
|
dataType,
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: { value: any; dataType: string } & Omit<
|
||||||
|
ComponentPropsWithoutRef<"div">,
|
||||||
|
"children"
|
||||||
|
>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"break-all",
|
||||||
|
["integer", "int", "tinyint", "double"].includes(dataType) &&
|
||||||
|
"text-right",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import { cn, isImageUrl, isUrl } from "@/lib/utils";
|
import { BodyCell } from "@/components/db-table-view/body-cell";
|
||||||
|
import { UrlWithPreview } from "@/components/db-table-view/url-with-preview";
|
||||||
|
import { cn, isUrl } from "@/lib/utils";
|
||||||
import { type TableColumns, useTableColumnsQuery } from "@/services/db";
|
import { type TableColumns, useTableColumnsQuery } from "@/services/db";
|
||||||
import { useSettingsStore } from "@/state";
|
import { useSettingsStore } from "@/state";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { useMemo } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
const buildColumns = <T,>({
|
const buildColumns = <T,>({
|
||||||
columns,
|
columns,
|
||||||
formatDates,
|
formatDates,
|
||||||
showImagesPreview,
|
|
||||||
}: {
|
}: {
|
||||||
columns?: TableColumns;
|
columns?: TableColumns;
|
||||||
|
|
||||||
formatDates: boolean;
|
formatDates: boolean;
|
||||||
showImagesPreview: boolean;
|
|
||||||
}): ColumnDef<T>[] => {
|
}): ColumnDef<T>[] => {
|
||||||
if (!columns) return [] as ColumnDef<T>[];
|
if (!columns) return [] as ColumnDef<T>[];
|
||||||
|
|
||||||
@@ -21,6 +20,7 @@ const buildColumns = <T,>({
|
|||||||
title: column_name,
|
title: column_name,
|
||||||
size: 300,
|
size: 300,
|
||||||
id: column_name,
|
id: column_name,
|
||||||
|
enableSorting: true,
|
||||||
header: () => {
|
header: () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -33,9 +33,35 @@ const buildColumns = <T,>({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
enableSorting: true,
|
cell: ({ row, table, column }) => {
|
||||||
cell: ({ row }) => {
|
const initialValue = row.getValue(column_name) as any;
|
||||||
const value = row.getValue(column_name) as any;
|
const [value, setValue] = useState(initialValue);
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
function handleDoubleClick() {
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
function handleSave() {
|
||||||
|
if (value === initialValue) return;
|
||||||
|
(table.options.meta as any)?.setEditedRows((old) => ({
|
||||||
|
...old,
|
||||||
|
[row.index]: true,
|
||||||
|
}));
|
||||||
|
(table.options.meta as any)?.updateData(row.index, column.id, value);
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
// biome-ignore lint/a11y/noAutofocus: <explanation>
|
||||||
|
autoFocus={true}
|
||||||
|
className={"w-full focus:ring"}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
onBlur={handleSave}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
let finalValue = value;
|
let finalValue = value;
|
||||||
if (
|
if (
|
||||||
formatDates &&
|
formatDates &&
|
||||||
@@ -43,43 +69,18 @@ const buildColumns = <T,>({
|
|||||||
) {
|
) {
|
||||||
finalValue = new Date(value as string).toLocaleString();
|
finalValue = new Date(value as string).toLocaleString();
|
||||||
}
|
}
|
||||||
if (showImagesPreview && typeof value === "string" && isUrl(value)) {
|
if (typeof value === "string" && isUrl(value)) {
|
||||||
const isImage = isImageUrl(value);
|
return <UrlWithPreview url={value} />;
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={value}
|
|
||||||
target={"_blank"}
|
|
||||||
className={cn("hover:underline")}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={"flex items-center justify-between break-all gap-4"}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
{isImage && (
|
|
||||||
<img
|
|
||||||
src={value}
|
|
||||||
alt={"preview"}
|
|
||||||
className="size-20 object-cover"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (typeof finalValue === "boolean") {
|
if (typeof finalValue === "boolean") {
|
||||||
finalValue = finalValue ? "true" : "false";
|
finalValue = finalValue ? "true" : "false";
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<BodyCell
|
||||||
className={cn(
|
value={finalValue}
|
||||||
"break-all",
|
dataType={data_type}
|
||||||
["integer", "int", "tinyint", "double"].includes(data_type) &&
|
onDoubleClick={handleDoubleClick}
|
||||||
"text-right",
|
/>
|
||||||
)}
|
|
||||||
>
|
|
||||||
{finalValue}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
})) as ColumnDef<T>[];
|
})) as ColumnDef<T>[];
|
||||||
@@ -91,13 +92,11 @@ export const useColumns = ({
|
|||||||
}: { dbName: string; tableName: string }) => {
|
}: { dbName: string; tableName: string }) => {
|
||||||
const { data: details } = useTableColumnsQuery({ dbName, tableName });
|
const { data: details } = useTableColumnsQuery({ dbName, tableName });
|
||||||
const formatDates = useSettingsStore.use.formatDates();
|
const formatDates = useSettingsStore.use.formatDates();
|
||||||
const showImagesPreview = useSettingsStore.use.showImagesPreview();
|
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return buildColumns({
|
return buildColumns({
|
||||||
columns: details,
|
columns: details,
|
||||||
formatDates,
|
formatDates,
|
||||||
showImagesPreview,
|
|
||||||
});
|
});
|
||||||
}, [details, formatDates, showImagesPreview]);
|
}, [details, formatDates]);
|
||||||
};
|
};
|
||||||
|
|||||||
28
frontend/src/components/db-table-view/url-with-preview.tsx
Normal file
28
frontend/src/components/db-table-view/url-with-preview.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { cn, isImageUrl } from "@/lib/utils";
|
||||||
|
import { useSettingsStore } from "@/state";
|
||||||
|
import { memo } from "react";
|
||||||
|
|
||||||
|
const RawUrlWithPreview = ({ url }: { url: string }) => {
|
||||||
|
const showImagesPreview = useSettingsStore.use.showImagesPreview();
|
||||||
|
const isImage = showImagesPreview && isImageUrl(url);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target={"_blank"}
|
||||||
|
className={cn("hover:underline")}
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<div className={"flex items-center justify-between break-all gap-4"}>
|
||||||
|
{url}
|
||||||
|
{isImage && (
|
||||||
|
<img src={url} alt={"preview"} className="size-20 object-cover" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UrlWithPreview = memo(RawUrlWithPreview);
|
||||||
|
|
||||||
|
UrlWithPreview.displayName = "UrlWithPreview";
|
||||||
@@ -86,6 +86,14 @@ function Component() {
|
|||||||
sortDesc: filters.sortDesc,
|
sortDesc: filters.sortDesc,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [tableData, setTableData] = useState<Array<Record<string, any>>>([]);
|
||||||
|
console.log(tableData);
|
||||||
|
const [editedRows, setEditedRows] = useState({});
|
||||||
|
console.log("editedRows", editedRows);
|
||||||
|
useEffect(() => {
|
||||||
|
setTableData(structuredClone(data?.data) ?? []);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const handleWhereClauseFormSubmit = useCallback(
|
const handleWhereClauseFormSubmit = useCallback(
|
||||||
({ whereClause }: WhereClauseFormValues) => {
|
({ whereClause }: WhereClauseFormValues) => {
|
||||||
if (whereClause === whereQuery) {
|
if (whereClause === whereQuery) {
|
||||||
@@ -120,9 +128,26 @@ function Component() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
meta: {
|
||||||
|
editedRows,
|
||||||
|
setEditedRows,
|
||||||
|
updateData: (rowIndex: number, columnId: string, value: string) => {
|
||||||
|
setTableData((old) =>
|
||||||
|
old.map((row, index) => {
|
||||||
|
if (index === rowIndex) {
|
||||||
|
return {
|
||||||
|
...old[rowIndex],
|
||||||
|
[columnId]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
columnResizeMode: "onChange",
|
columnResizeMode: "onChange",
|
||||||
columns,
|
columns,
|
||||||
data: data?.data ?? [],
|
data: tableData,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
manualSorting: true,
|
manualSorting: true,
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export const useTablesListQuery = (args: GetTablesListArgs) => {
|
|||||||
|
|
||||||
export const useTableDataQuery = (args: GetTableDataArgs) => {
|
export const useTableDataQuery = (args: GetTableDataArgs) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchInterval: false,
|
||||||
|
refetchIntervalInBackground: false,
|
||||||
|
staleTime: Number.POSITIVE_INFINITY,
|
||||||
queryKey: [DB_QUERY_KEYS.TABLES.DATA, args],
|
queryKey: [DB_QUERY_KEYS.TABLES.DATA, args],
|
||||||
queryFn: () => dbService.getTableData(args),
|
queryFn: () => dbService.getTableData(args),
|
||||||
placeholderData: (previousData, previousQuery) => {
|
placeholderData: (previousData, previousQuery) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user