Skip to content

Commit

Permalink
add trigger and improve search
Browse files Browse the repository at this point in the history
  • Loading branch information
invisal committed Apr 10, 2024
1 parent 268b894 commit d68d6fe
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 50 deletions.
145 changes: 117 additions & 28 deletions src/components/schema-sidebar-list.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,55 @@
import { ScrollArea } from "./ui/scroll-area";
import { buttonVariants } from "./ui/button";
import { cn } from "@/lib/utils";
import { LucideIcon, Table2 } from "lucide-react";
import { LucideCog, LucideIcon, LucideView, Table2 } from "lucide-react";
import {
OpenContextMenuList,
openContextMenuFromEvent,
} from "@/messages/openContextMenu";
import { useSchema } from "@/context/SchemaProvider";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { openTab } from "@/messages/open-tab";
import { DatabaseSchemaItem } from "@/drivers/base-driver";

interface SchemaListProps {
search: string;
}

type DatabaseSchemaTreeNode = {
node: DatabaseSchemaItem;
sub: DatabaseSchemaItem[];
};

interface SchemaViewItemProps {
item: DatabaseSchemaItem;
highlight?: string;
icon: LucideIcon;
iconClassName?: string;
title: string;
selected: boolean;
onClick: () => void;
onContextMenu: React.MouseEventHandler;
indentation?: boolean;
}

function SchemaViewItem({
icon: Icon,
iconClassName,
title,
onClick,
highlight,
selected,
onContextMenu,
indentation,
item,
}: Readonly<SchemaViewItemProps>) {
const regex = new RegExp(
"(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")",
"i"
);

const splitedText = title.split(regex);

return (
<div
onMouseDown={onClick}
Expand All @@ -37,10 +58,17 @@ function SchemaViewItem({
onClick();
}}
onDoubleClick={() => {
openTab({
type: "table",
tableName: title,
});
if (item.type === "table" || item.type === "view") {
openTab({
type: "table",
tableName: title,
});
} else if (item.type === "trigger") {
openTab({
type: "trigger",
name: item.name,
});
}
}}
className={cn(
buttonVariants({
Expand All @@ -51,8 +79,21 @@ function SchemaViewItem({
"cursor-pointer"
)}
>
<Icon className="mr-2 h-4 w-4" />
{title}
{indentation && (
<div className="w-2 border-l ml-2 2 h-full border-dashed"></div>
)}
<Icon className={cn("mr-2 h-4 w-4", selected ? "" : iconClassName)} />
<span>
{splitedText.map((text, idx) => {
return text.toLowerCase() === (highlight ?? "").toLowerCase() ? (
<span key={idx} className="bg-yellow-300 text-black">
{text}
</span>
) : (
<span key={idx}>{text}</span>
);
})}
</span>
</div>
);
}
Expand All @@ -66,13 +107,16 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
}, [setSelectedIndex, search]);

const prepareContextMenu = useCallback(
(tableName?: string) => {
(item?: DatabaseSchemaItem) => {
const selectedName = item?.name;
const isTable = item?.type === "table";

return [
{
title: "Copy Name",
disabled: !tableName,
disabled: !selectedName,
onClick: () => {
window.navigator.clipboard.writeText(tableName ?? "");
window.navigator.clipboard.writeText(selectedName ?? "");
},
},
{ separator: true },
Expand All @@ -84,26 +128,57 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
});
},
},
{
title: "Edit Table",
disabled: !tableName,
onClick: () => {
openTab({
tableName,
type: "schema",
});
},
},
isTable
? {
title: "Edit Table",
onClick: () => {
openTab({
tableName: item?.name,
type: "schema",
});
},
}
: undefined,
{ separator: true },
{ title: "Refresh", onClick: () => refresh() },
] as OpenContextMenuList;
].filter(Boolean) as OpenContextMenuList;
},
[refresh]
);

const filteredSchema = schema.filter((s) => {
return s.name.toLowerCase().indexOf(search.toLowerCase()) >= 0;
});
const filteredSchema = useMemo(() => {
// Build the tree first then we can flat it
let tree: DatabaseSchemaTreeNode[] = [];
const treeHash: Record<string, DatabaseSchemaTreeNode> = {};

for (const item of schema) {
if (item.type === "table" || item.type === "view") {
const node = { node: item, sub: [] };
treeHash[item.name] = node;
tree.push(node);
}
}

for (const item of schema) {
if (item.type === "trigger" && item.tableName) {
if (treeHash[item.tableName]) {
treeHash[item.tableName].sub.push(item);
}
}
}

tree = tree.filter((s) => {
const foundName =
s.node.name.toLowerCase().indexOf(search.toLowerCase()) >= 0;
const foundInChildren =
s.sub.filter(
(c) => c.name.toLowerCase().indexOf(search.toLowerCase()) >= 0
).length > 0;
return foundName || foundInChildren;
});

return tree.map((r) => [r.node, ...r.sub]).flat();
}, [schema, search]);

return (
<ScrollArea
Expand All @@ -112,23 +187,37 @@ export default function SchemaList({ search }: Readonly<SchemaListProps>) {
openContextMenuFromEvent(
prepareContextMenu(
selectedIndex && schema[selectedIndex]
? schema[selectedIndex].name
? schema[selectedIndex]
: undefined
)
)(e)
}
>
<div className="flex flex-col p-2 pr-4">
{filteredSchema.map((item, schemaIndex) => {
let icon = Table2;
let iconClassName = "text-blue-600 dark:text-blue-300";
if (item.type === "trigger") {
icon = LucideCog;
iconClassName = "text-purple-500";
} else if (item.type === "view") {
icon = LucideView;
iconClassName = "text-green-600 dark:text-green-300";
}

return (
<SchemaViewItem
highlight={search}
item={item}
onContextMenu={(e) => {
openContextMenuFromEvent(prepareContextMenu(item.name))(e);
openContextMenuFromEvent(prepareContextMenu(item))(e);
e.stopPropagation();
}}
key={item.name}
title={item.name}
icon={Table2}
iconClassName={iconClassName}
icon={icon}
indentation={item.type === "trigger"}
selected={schemaIndex === selectedIndex}
onClick={() => setSelectedIndex(schemaIndex)}
/>
Expand Down
74 changes: 74 additions & 0 deletions src/components/tabs/trigger-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useDatabaseDriver } from "@/context/DatabaseDriverProvider";
import { DatabaseTriggerSchema } from "@/drivers/base-driver";
import { useEffect, useState } from "react";
import { Input } from "../ui/input";
import SqlEditor from "../sql-editor";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../ui/select";
import TableCombobox from "../table-combobox/TableCombobox";

export default function TriggerTab({ name }: { name: string }) {
const { databaseDriver } = useDatabaseDriver();
const [trigger, setTrigger] = useState<DatabaseTriggerSchema>();
const [error, setError] = useState<string>();

useEffect(() => {
databaseDriver
.trigger(name)
.then(setTrigger)
.catch((e: Error) => {
setError(e.message);
});
}, [databaseDriver, name]);

if (error) {
return <div className="p-4">{error}</div>;
}

return (
<div className="flex flex-col overflow-hidden w-full h-full">
<div className="p-4 flex flex-col gap-2">
<div className="text-xs text-gray-800">Trigger Name</div>
<Input value={trigger?.name ?? ""} readOnly />

<div className="flex gap-2">
<div className="w-[200px]">
<Select value={trigger?.when ?? "BEFORE"}>
<SelectTrigger>
<SelectValue placeholder="When" />
</SelectTrigger>
<SelectContent>
<SelectItem value="BEFORE">Before</SelectItem>
<SelectItem value="AFTER">After</SelectItem>
<SelectItem value="INSTEAD_OF">Instead Of</SelectItem>
</SelectContent>
</Select>
</div>
<div className="w-[200px]">
<Select value={trigger?.operation}>
<SelectTrigger>
<SelectValue placeholder="Operation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="INSERT">Insert</SelectItem>
<SelectItem value="UPDATE">Update</SelectItem>
<SelectItem value="DELETE">Delete</SelectItem>
</SelectContent>
</Select>
</div>
<TableCombobox value={trigger?.tableName} onChange={() => {}} />
</div>
</div>
<div className="grow overflow-hidden">
<div className="h-full">
<SqlEditor value={trigger?.statement ?? ""} />
</div>
</div>
</div>
);
}
17 changes: 17 additions & 0 deletions src/drivers/base-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export interface SelectFromTableOptions {
export type DatabaseValue<T = unknown> = T | undefined | null;

export interface DatabaseSchemaItem {
type: "table" | "trigger" | "view";
name: string;
tableName?: string;
}

export interface DatabaseTableColumn {
Expand Down Expand Up @@ -108,6 +110,20 @@ export interface DatabaseTableSchema {
createScript?: string;
}

export type TriggerWhen = "BEFORE" | "AFTER" | "INSTEAD_OF";

export type TriggerOperation = "INSERT" | "UPDATE" | "DELETE";

export interface DatabaseTriggerSchema {
name: string;
operation: TriggerOperation;
when: TriggerWhen;
tableName: string;
columnNames?: string[];
whenExpression: string;
statement: string;
}

interface DatabaseTableOperationInsert {
operation: "INSERT";
values: Record<string, DatabaseValue>;
Expand Down Expand Up @@ -147,6 +163,7 @@ export abstract class BaseDriver {

abstract schemas(): Promise<DatabaseSchemaItem[]>;
abstract tableSchema(tableName: string): Promise<DatabaseTableSchema>;
abstract trigger(name: string): Promise<DatabaseTriggerSchema>;

abstract selectTable(
tableName: string,
Expand Down
Loading

0 comments on commit d68d6fe

Please sign in to comment.