From 2c5f325afba9e22d5a8de2a3be079fb981ccb1b3 Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Fri, 27 Dec 2024 10:02:05 +0700 Subject: [PATCH 1/6] feat: custom connection folder --- src/components/create-folder-button.tsx | 18 ++++++ .../database/create-folder-modal.tsx | 44 ++++++++++++++ src/components/folder.tsx | 57 ++++++++++++++++++ src/database/index.tsx | 58 +++++++++++-------- 4 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 src/components/create-folder-button.tsx create mode 100644 src/components/database/create-folder-modal.tsx create mode 100644 src/components/folder.tsx diff --git a/src/components/create-folder-button.tsx b/src/components/create-folder-button.tsx new file mode 100644 index 0000000..b9aee76 --- /dev/null +++ b/src/components/create-folder-button.tsx @@ -0,0 +1,18 @@ +import { FolderIcon } from "lucide-react"; +import { Button } from "./ui/button"; +import CreateFolderModal from "./database/create-folder-modal"; +import { useState } from "react"; + +interface Props {} +export default function CreateFolderButton(props: Props) { + const [visible, setVisible] = useState(false); + return ( +
+ + setVisible(false)} /> +
+ ); +} diff --git a/src/components/database/create-folder-modal.tsx b/src/components/database/create-folder-modal.tsx new file mode 100644 index 0000000..8bf3010 --- /dev/null +++ b/src/components/database/create-folder-modal.tsx @@ -0,0 +1,44 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import { Input } from "../ui/input"; + +export default function CreateFolderModal({ + visible, + onClose, +}: { + visible: boolean; + onClose: () => void; +}) { + return ( + { + if (!openState) { + onClose(); + } + }} + > + + + Create Folder + + Creating folder helping you to organize your database connection + + + + + {}}>Cancel + {}}>Create + + + + ); +} diff --git a/src/components/folder.tsx b/src/components/folder.tsx new file mode 100644 index 0000000..0efea22 --- /dev/null +++ b/src/components/folder.tsx @@ -0,0 +1,57 @@ +import { FolderIcon } from "lucide-react"; +import { Button } from "./ui/button"; +import React, { useMemo, useState } from "react"; +import { ConnectionStoreItem } from "@/lib/conn-manager-store"; +import { Input } from "./ui/input"; + +interface Props { + data: ConnectionStoreItem[]; + renderItem: (data: ConnectionStoreItem[]) => React.ReactElement; +} + +interface Folder { + title: string; + data: ConnectionStoreItem[]; +} + +const connectionTypeList = [ + "mysql", + "postgres", + "sqlite", + "turso", + "cloudflare", + "starbase", +]; +export default function Folder({ data, renderItem }: Props) { + const [activeType, setActiveType] = useState("mysql"); + + const filterConnections = useMemo(() => { + return data.filter((item) => item.type === activeType); + }, [data, activeType]); + + return ( +
+
+ {/* */} +
+ +
+ {connectionTypeList.map((conn, index) => { + const active = conn === activeType; + return ( + + ); + })} +
+
{renderItem(filterConnections)}
+
+ ); +} diff --git a/src/database/index.tsx b/src/database/index.tsx index 9f771c6..a51d009 100644 --- a/src/database/index.tsx +++ b/src/database/index.tsx @@ -25,6 +25,7 @@ import { } from "@dnd-kit/sortable"; import { MySQLIcon } from "@/lib/outerbase-icon"; import { + FolderIcon, LucideCopy, LucideMoreHorizontal, LucidePencil, @@ -57,6 +58,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { useToast } from "@/hooks/use-toast"; import { cn } from "@/lib/utils"; import ImportConnectionStringRoute from "./import-connection-string"; +import Folder from "@/components/folder"; const connectionTypeList = [ "mysql", @@ -100,7 +102,6 @@ function DeletingModal({ {data.name}? - Cancel Continue @@ -336,30 +337,37 @@ function ConnectionListRoute() { )}
- - - - {connectionList.map((item) => ( - - ))} - - - + { + return ( + + + + {data.map((item) => ( + + ))} + + + + ); + }} + />
); From 7c64e56d0886a59592ea0a34d14bde3e7590883d Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Sat, 28 Dec 2024 21:01:50 +0700 Subject: [PATCH 2/6] moved connection item to separate file --- src/components/database/connection-item.tsx | 172 ++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/components/database/connection-item.tsx diff --git a/src/components/database/connection-item.tsx b/src/components/database/connection-item.tsx new file mode 100644 index 0000000..ae61b4a --- /dev/null +++ b/src/components/database/connection-item.tsx @@ -0,0 +1,172 @@ +import { useState } from "react"; +import { + ConnectionStoreItem, + ConnectionStoreManager, + connectionTypeTemplates, +} from "@/lib/conn-manager-store"; +import { useSortable } from "@dnd-kit/sortable"; +import { useToast } from "@/hooks/use-toast"; +import { useNavigate } from "react-router-dom"; +import { MySQLIcon } from "@/lib/outerbase-icon"; +import { Button } from "../ui/button"; +import { + LucideCopy, + LucideMoreHorizontal, + LucidePencil, + LucideTrash, +} from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { motion } from "framer-motion"; +import { CSS } from "@dnd-kit/utilities"; +import { generateConnectionString } from "@/lib/connection-string"; +import { cn } from "@/lib/utils"; + +export default function ConnectionItem({ + item, + selectedConnection, + setSelectedConnection, + setConnectionList, + setDeletingConnectionId, +}: { + item: ConnectionStoreItem; + selectedConnection?: string; + setSelectedConnection: DispatchState; + setConnectionList: DispatchState; + setDeletingConnectionId: DispatchState; +}) { + const [isMenuOpen, setMenuOpen] = useState(false); + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: item.id }); + + const { toast } = useToast(); + const navigate = useNavigate(); + const typeConfig = connectionTypeTemplates[item.type]; + const IconComponent = typeConfig?.icon ?? MySQLIcon; + + function onConnect() { + const connItem: ConnectionStoreItem = { + ...item, + lastConnectAt: new Date().getTime(), + }; + ConnectionStoreManager.save(connItem); + window.outerbaseIpc.connect(connItem); + } + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
{ + setSelectedConnection(item.id); + }} + onDoubleClick={onConnect} + > + + +
+
{item.name}
+
+ {generateConnectionString(item)} +
+
+
+ + + + + + + Connect + + + { + window.outerbaseIpc.connect(item, true); + }} + > + Connect with debugger + + + + { + navigate(`/connection/edit/${item.type}/${item.id}`); + }} + > + + Edit + + + { + setConnectionList(ConnectionStoreManager.duplicate(item)); + }} + inset + > + Duplicate + + + + { + window.navigator.clipboard.writeText( + generateConnectionString(item, false), + ); + toast({ + title: "Connection string copied to clipboard", + duration: 1000, + }); + }} + > + + Copy Connection String + + + + { + setDeletingConnectionId(item); + }} + > + + Delete + + + +
+
+
+ ); +} From 89bb9e74f029569b225f7e55524c3f3d712cfde3 Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Sat, 28 Dec 2024 21:02:46 +0700 Subject: [PATCH 3/6] moved connection list to separate file --- src/components/database/connection-list.tsx | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/components/database/connection-list.tsx diff --git a/src/components/database/connection-list.tsx b/src/components/database/connection-list.tsx new file mode 100644 index 0000000..03a783f --- /dev/null +++ b/src/components/database/connection-list.tsx @@ -0,0 +1,92 @@ +import { useState } from "react"; +import { + ConnectionStoreItem, + ConnectionStoreManager, +} from "@/lib/conn-manager-store"; +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + Modifier, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { AnimatePresence } from "framer-motion"; +import ConnectionItem from "./connection-item"; +import DeletingConnectionModal from "./delect-connection-modal"; + +interface Props { + data: ConnectionStoreItem[]; + onDragEnd?: (event: DragEndEvent) => void; + setConnectionList: DispatchState; +} + +const restrictToVerticalAxis: Modifier = ({ transform }) => { + return { + ...transform, + x: 0, + }; +}; + +export default function ConnectionList({ + data, + onDragEnd, + setConnectionList, +}: Props) { + const [selectedConnection, setSelectedConnection] = useState(""); + const [deletingConnectionId, setDeletingConnectionId] = + useState(null); + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }), + ); + + return ( + <> + {deletingConnectionId && ( + { + setDeletingConnectionId(null); + }} + onSuccess={() => { + setConnectionList( + ConnectionStoreManager.remove(deletingConnectionId.id), + ); + setDeletingConnectionId(null); + }} + /> + )} + + + + {data.map((item) => ( + + ))} + + + + + ); +} From 71c842f87e405647eeceabcb4aba60ee52a7f2ea Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Sat, 28 Dec 2024 21:03:21 +0700 Subject: [PATCH 4/6] moved delete connection modal to separate file --- .../database/delect-connection-modal.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/components/database/delect-connection-modal.tsx diff --git a/src/components/database/delect-connection-modal.tsx b/src/components/database/delect-connection-modal.tsx new file mode 100644 index 0000000..f90b644 --- /dev/null +++ b/src/components/database/delect-connection-modal.tsx @@ -0,0 +1,48 @@ +import { ConnectionStoreItem } from "@/lib/conn-manager-store"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; + +interface Props { + data: ConnectionStoreItem; + onClose: () => void; + onSuccess: () => void; +} + +export default function DeletingConnectionModal({ + data, + onClose, + onSuccess, +}: Props) { + return ( + { + if (openState === false) { + onClose(); + } + }} + > + + + Delete Connection + + Are you sure you want to delete the connection{" "} + {data.name}? + + + + Cancel + Continue + + + + ); +} From 91b29565f15648e7fef73886c80a753fceb60c32 Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Sat, 28 Dec 2024 21:04:38 +0700 Subject: [PATCH 5/6] custom resizeable panel --- src/components/resizeable-panel.tsx | 163 ++++++++++++++++++++++++++++ src/hooks/useWindowDimension.ts | 23 ++++ 2 files changed, 186 insertions(+) create mode 100644 src/components/resizeable-panel.tsx create mode 100644 src/hooks/useWindowDimension.ts diff --git a/src/components/resizeable-panel.tsx b/src/components/resizeable-panel.tsx new file mode 100644 index 0000000..f287bf3 --- /dev/null +++ b/src/components/resizeable-panel.tsx @@ -0,0 +1,163 @@ +import React, { + PropsWithChildren, + ReactNode, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { cn } from "@/lib/utils"; +import useWindowDimension from "@/hooks/useWindowDimension"; + +interface PanelGroupProps { + direction: "horizontal" | "vertical"; + children: ReactNode; +} + +const PanelGroup = ({ + direction, + children, +}: PropsWithChildren) => { + const [panelSizes, setPanelSizes] = useState([]); + const { width } = useWindowDimension(); + + const handleResize = (index: number, delta: number) => { + setPanelSizes((prevSizes) => { + const newSizes = [...prevSizes]; + newSizes[index] = Math.max(newSizes[index] + delta, 100); + if (index + 1 < newSizes.length) { + newSizes[index + 1] = Math.max(newSizes[index + 1] - delta, 100); + } + return newSizes; + }); + }; + + const panels = useMemo( + () => React.Children.toArray(children) as React.ReactElement[], + [children], + ); + + useEffect(() => { + setPanelSizes(panels.map(() => width / panels.length)); + }, [width]); + + return ( +
+ {panels.map((panel, index) => ( + + {React.cloneElement(panel, { + size: panelSizes[index], // Pass the size as a prop to Panel + })} + {index < panels.length - 1 && ( + handleResize(index, delta)} + direction={direction} + /> + )} + + ))} +
+ ); +}; + +interface PanelProps { + id?: string; + minSize?: number; + maxSize?: number; + size?: number; + grow?: boolean; + children: ReactNode; + className?: string; +} + +const Panel = ({ + id, + minSize = 100, + maxSize, + size, + grow = false, + children, + className, +}: PropsWithChildren) => { + return ( +
+ {children} +
+ ); +}; + +interface PanelResizeHandleProps { + onResize: (delta: number) => void; + direction: "horizontal" | "vertical"; +} + +const PanelResizeHandle = ({ + onResize, + direction = "horizontal", +}: PropsWithChildren) => { + const isResizing = useRef(false); + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + isResizing.current = true; + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!isResizing.current) return; + + const delta = direction === "horizontal" ? e.movementX : e.movementY; + onResize(delta); + }; + + const handleMouseUp = () => { + isResizing.current = false; + }; + + useEffect(() => { + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, []); + + return ( +
+ ); +}; + +PanelGroup.Panel = Panel; + +export { PanelGroup }; diff --git a/src/hooks/useWindowDimension.ts b/src/hooks/useWindowDimension.ts new file mode 100644 index 0000000..6c79784 --- /dev/null +++ b/src/hooks/useWindowDimension.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react"; + +export default function useWindowDimension() { + const [dimension, setDimension] = useState<{ width: number; height: number }>( + { + width: window.innerWidth, + height: window.innerHeight, + }, + ); + useEffect(() => { + const handleResize = () => { + setDimension({ + width: window.innerWidth, + height: window.innerHeight, + }); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + return dimension; +} From 048f947fbff84c55d959a1a16d06c8b20cd376a8 Mon Sep 17 00:00:00 2001 From: "@roth-dev" Date: Thu, 2 Jan 2025 21:34:39 +0700 Subject: [PATCH 6/6] filter connections --- src/components/folder.tsx | 45 +++-- src/database/index.tsx | 344 ++++++--------------------------- src/lib/conn-manager-store.tsx | 12 +- vite-env.d.ts | 4 + 4 files changed, 98 insertions(+), 307 deletions(-) diff --git a/src/components/folder.tsx b/src/components/folder.tsx index 0efea22..6d2e962 100644 --- a/src/components/folder.tsx +++ b/src/components/folder.tsx @@ -1,12 +1,14 @@ import { FolderIcon } from "lucide-react"; import { Button } from "./ui/button"; -import React, { useMemo, useState } from "react"; import { ConnectionStoreItem } from "@/lib/conn-manager-store"; import { Input } from "./ui/input"; +import { cn } from "@/lib/utils"; interface Props { - data: ConnectionStoreItem[]; - renderItem: (data: ConnectionStoreItem[]) => React.ReactElement; + search: string; + selected: string; + setSearch: (search: string) => void; + onChangeFolder: (folder: string) => void; } interface Folder { @@ -15,6 +17,7 @@ interface Folder { } const connectionTypeList = [ + "recent", "mysql", "postgres", "sqlite", @@ -22,28 +25,37 @@ const connectionTypeList = [ "cloudflare", "starbase", ]; -export default function Folder({ data, renderItem }: Props) { - const [activeType, setActiveType] = useState("mysql"); - - const filterConnections = useMemo(() => { - return data.filter((item) => item.type === activeType); - }, [data, activeType]); +export default function Folder({ + selected, + search, + setSearch, + onChangeFolder, +}: Props) { return ( -
-
- {/* */} +
+
- + { + e.preventDefault(); + setSearch(e.target.value); + }} + />
{connectionTypeList.map((conn, index) => { - const active = conn === activeType; + const active = conn === selected; return (
-
{renderItem(filterConnections)}
); } diff --git a/src/database/index.tsx b/src/database/index.tsx index a51d009..45bc220 100644 --- a/src/database/index.tsx +++ b/src/database/index.tsx @@ -1,64 +1,26 @@ import { Toolbar, ToolbarDropdown } from "@/components/toolbar"; import { - DropdownMenu, - DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, - DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { - closestCenter, - DndContext, - DragEndEvent, - KeyboardSensor, - Modifier, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; +import { DragEndEvent } from "@dnd-kit/core"; +import { arrayMove } from "@dnd-kit/sortable"; import { MySQLIcon } from "@/lib/outerbase-icon"; -import { - FolderIcon, - LucideCopy, - LucideMoreHorizontal, - LucidePencil, - LucidePlus, - LucideTrash, -} from "lucide-react"; +import { LucidePlus } from "lucide-react"; import { AnimatedRouter } from "@/components/animated-router"; import { ConnectionCreateUpdateRoute } from "./editor-route"; import { useNavigate } from "react-router-dom"; -import { CSS } from "@dnd-kit/utilities"; import { ConnectionStoreItem, ConnectionStoreManager, connectionTypeTemplates, } from "@/lib/conn-manager-store"; -import { Dispatch, SetStateAction, useState } from "react"; -import { generateConnectionString } from "@/lib/connection-string"; -import { Button } from "@/components/ui/button"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; -import { AnimatePresence, motion } from "framer-motion"; -import { useToast } from "@/hooks/use-toast"; -import { cn } from "@/lib/utils"; +import { useEffect, useMemo, useState } from "react"; + import ImportConnectionStringRoute from "./import-connection-string"; import Folder from "@/components/folder"; +import { PanelGroup } from "@/components/resizeable-panel"; +import ConnectionList from "@/components/database/connection-list"; const connectionTypeList = [ "mysql", @@ -69,209 +31,26 @@ const connectionTypeList = [ "starbase", ]; -const restrictToVerticalAxis: Modifier = ({ transform }) => { - return { - ...transform, - x: 0, - }; -}; - -function DeletingModal({ - data, - onClose, - onSuccess, -}: { - data: ConnectionStoreItem; - onClose: () => void; - onSuccess: () => void; -}) { - return ( - { - if (openState === false) { - onClose(); - } - }} - > - - - Delete Connection - - Are you sure you want to delete the connection{" "} - {data.name}? - - - - Cancel - Continue - - - - ); -} - -function ConnectionItem({ - item, - selectedConnection, - setSelectedConnection, - setConnectionList, - setDeletingConnectionId, -}: { - item: ConnectionStoreItem; - selectedConnection?: string; - setSelectedConnection: Dispatch>; - setConnectionList: Dispatch>; - setDeletingConnectionId: Dispatch>; -}) { - const [isMenuOpen, setMenuOpen] = useState(false); - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id: item.id }); - - const { toast } = useToast(); - const navigate = useNavigate(); - const typeConfig = connectionTypeTemplates[item.type]; - const IconComponent = typeConfig?.icon ?? MySQLIcon; - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - return ( -
{ - setSelectedConnection(item.id); - }} - onDoubleClick={() => { - window.outerbaseIpc.connect(item); - }} - > - - -
-
{item.name}
-
- {generateConnectionString(item)} -
-
-
- - - - - - { - window.outerbaseIpc.connect(item); - }} - > - Connect - - - { - window.outerbaseIpc.connect(item, true); - }} - > - Connect with debugger - - - - { - navigate(`/connection/edit/${item.type}/${item.id}`); - }} - > - - Edit - - - { - setConnectionList(ConnectionStoreManager.duplicate(item)); - }} - inset - > - Duplicate - - - - { - window.navigator.clipboard.writeText( - generateConnectionString(item, false), - ); - toast({ - title: "Connection string copied to clipboard", - duration: 1000, - }); - }} - > - - Copy Connection String - - - - { - setDeletingConnectionId(item); - }} - > - - Delete - - - -
-
-
- ); -} - function ConnectionListRoute() { - const [connectionList, setConnectionList] = useState(() => { - return ConnectionStoreManager.list(); - }); + const [search, setSearch] = useState(""); - const [selectedConnection, setSelectedConnection] = useState(""); + const [selectedFolder, setSelectedFolder] = useState("recent"); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), + const [connectionList, setConnectionList] = useState( + [], ); - const [deletingConnectionId, setDeletingConnectionId] = - useState(null); - const navigate = useNavigate(); + useEffect(() => { + const list = ConnectionStoreManager.list(); + setConnectionList(list); + }, []); + + function onChangeFolder(folder: string) { + setSelectedFolder(folder); + } + function handleDragEnd(event: DragEndEvent) { const { active, over } = event; @@ -287,6 +66,21 @@ function ConnectionListRoute() { } } + const filterdConnection = useMemo(() => { + if (search) { + return connectionList.filter((conn) => { + return conn.name.toLowerCase().includes(search.toLowerCase()); + }); + } + if (selectedFolder === "recent") { + return connectionList.slice(0, 10); + } + const newList = connectionList.filter( + (conn) => conn.type === selectedFolder, + ); + return newList; + }, [search, selectedFolder, connectionList]); + return (
@@ -321,54 +115,26 @@ function ConnectionListRoute() { - {deletingConnectionId && ( - { - setDeletingConnectionId(null); - }} - onSuccess={() => { - setConnectionList( - ConnectionStoreManager.remove(deletingConnectionId.id), - ); - setDeletingConnectionId(null); - }} - /> - )} - -
- { - return ( - - - - {data.map((item) => ( - - ))} - - - - ); - }} - /> -
+ + + + + + { + // render connection list + } + + +
); } diff --git a/src/lib/conn-manager-store.tsx b/src/lib/conn-manager-store.tsx index b1da672..5b31c48 100644 --- a/src/lib/conn-manager-store.tsx +++ b/src/lib/conn-manager-store.tsx @@ -20,6 +20,7 @@ export interface ConnectionStoreItem { name: string; type: string; config: ConnectionStoreItemConfig; + lastConnectAt?: number; } interface ConnectionTypeTemplate { @@ -258,7 +259,6 @@ export class ConnectionStoreManager { static save(item: ConnectionStoreItem) { const list = this.list(); const index = list.findIndex((i) => i.id === item.id); - if (index === -1) { list.unshift(item); } else { @@ -271,4 +271,14 @@ export class ConnectionStoreManager { static saveAll(items: ConnectionStoreItem[]) { localStorage.setItem("connections", JSON.stringify(items)); } + + static sort(list: ConnectionStoreItem[]) { + if (list.length === 0) return []; + + return list.sort( + (a, b) => + new Date(b.lastConnectAt || 0).getTime() - + new Date(a.lastConnectAt || 0).getTime(), + ); + } } diff --git a/vite-env.d.ts b/vite-env.d.ts index e8fc772..2da1f5e 100644 --- a/vite-env.d.ts +++ b/vite-env.d.ts @@ -1,5 +1,9 @@ +import { Dispatch, SetStateAction } from "react"; import { type OuterbaseIpc } from "./electron/preload"; declare global { + type Maybe = T | null | undefined; + + type DispatchState = Dispatch>; interface Window { outerbaseIpc: OuterbaseIpc; }