refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
import type { Connection, Folder, Tag } from "../../lib/types";
|
||||
import { Folder as FolderIcon, Check, Pencil, Trash2 } from "lucide-react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import { ConnectionCard } from "./ConnectionCard";
|
||||
import { FolderBreadcrumb } from "../folders/FolderBreadcrumb";
|
||||
import { getChildFolders, getDescendantFolderIds } from "../../lib/utils";
|
||||
import { useUiStore } from "../../stores/uiStore";
|
||||
import { useConnectionStore } from "../../stores/connectionStore";
|
||||
import { TagBadge } from "../tags/TagBadge";
|
||||
|
||||
interface DroppableFolderCardProps {
|
||||
folder: Folder;
|
||||
isSelected: boolean;
|
||||
count: number;
|
||||
subfolderCount: number;
|
||||
folderTags: Tag[];
|
||||
onFolderClick: (id: string) => void;
|
||||
onToggleSelection: (id: string) => void;
|
||||
}
|
||||
|
||||
function DroppableFolderCard({
|
||||
folder,
|
||||
isSelected,
|
||||
count,
|
||||
subfolderCount,
|
||||
folderTags,
|
||||
onFolderClick,
|
||||
onToggleSelection,
|
||||
}: DroppableFolderCardProps) {
|
||||
const { setNodeRef, isOver } = useDroppable({
|
||||
id: `folder-${folder.id}`,
|
||||
data: { type: "folder", folder },
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={`relative group rounded-xl border transition-colors ${
|
||||
isSelected
|
||||
? "bg-accent/10 border-accent"
|
||||
: "bg-surface border-border hover:border-border-hover"
|
||||
} ${isOver ? "ring-1 ring-accent bg-accent/10" : ""}`}
|
||||
>
|
||||
<button
|
||||
onClick={() => onFolderClick(folder.id)}
|
||||
className="w-full p-3 text-left min-w-0 cursor-pointer"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderIcon
|
||||
size={18}
|
||||
className={
|
||||
isSelected
|
||||
? "text-accent"
|
||||
: "text-text-muted"
|
||||
}
|
||||
/>
|
||||
<span className="font-semibold text-sm truncate text-text">
|
||||
{folder.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-1">
|
||||
{count > 0 &&
|
||||
`${count} item${count !== 1 ? "s" : ""}`}
|
||||
{count > 0 && subfolderCount > 0 && " · "}
|
||||
{subfolderCount > 0 &&
|
||||
`${subfolderCount} subfolder${subfolderCount !== 1 ? "s" : ""}`}
|
||||
{count === 0 &&
|
||||
subfolderCount === 0 &&
|
||||
"Empty folder"}
|
||||
</div>
|
||||
{folderTags.length > 0 && (
|
||||
<div className="flex gap-1 flex-wrap mt-2">
|
||||
{folderTags.map((t) => (
|
||||
<TagBadge key={t.id} tag={t} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleSelection(folder.id);
|
||||
}}
|
||||
className={`absolute -top-1.5 -left-1.5 w-4 h-4 rounded border flex items-center justify-center transition-all ${
|
||||
isSelected
|
||||
? "bg-accent border-accent opacity-100"
|
||||
: "border-border bg-surface opacity-0 group-hover:opacity-100"
|
||||
}`}
|
||||
>
|
||||
{isSelected && (
|
||||
<Check
|
||||
size={12}
|
||||
className="text-white"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConnectionGridProps {
|
||||
connections: Connection[];
|
||||
tags: Tag[];
|
||||
folders?: Folder[];
|
||||
activeFolderId?: string | null;
|
||||
onFolderSelect?: (id: string | null) => void;
|
||||
hasSearch?: boolean;
|
||||
onTagToggle?: (id: string) => void;
|
||||
onEditFolder?: (folder: Folder) => void;
|
||||
onDeleteFolder?: (folder: Folder) => void;
|
||||
onOpenDbViewer?: (connectionId: string) => void;
|
||||
onEditConnection?: (conn: Connection) => void;
|
||||
onDuplicateConnection?: (conn: Connection) => void;
|
||||
onDeleteConnection?: (conn: Connection) => void;
|
||||
}
|
||||
|
||||
export function ConnectionGrid({
|
||||
connections,
|
||||
tags,
|
||||
folders = [],
|
||||
activeFolderId = null,
|
||||
onFolderSelect,
|
||||
hasSearch = false,
|
||||
onTagToggle,
|
||||
onEditFolder,
|
||||
onDeleteFolder,
|
||||
onOpenDbViewer,
|
||||
onEditConnection,
|
||||
onDuplicateConnection,
|
||||
onDeleteConnection,
|
||||
}: ConnectionGridProps) {
|
||||
const selectedItemIds = useUiStore((s) => s.selectedItemIds);
|
||||
const toggleItemSelection = useUiStore((s) => s.toggleItemSelection);
|
||||
const clearSelection = useUiStore((s) => s.clearSelection);
|
||||
const activeTagIds = useUiStore((s) => s.activeTagIds);
|
||||
const activeDbTypes = useUiStore((s) => s.activeDbTypes);
|
||||
const activeEnvironment = useUiStore((s) => s.activeEnvironment);
|
||||
|
||||
const currentFolderId = hasSearch
|
||||
? null
|
||||
: activeFolderId !== null && folders.some((f) => f.id === activeFolderId)
|
||||
? activeFolderId
|
||||
: null;
|
||||
const hasActiveFilters =
|
||||
activeTagIds.length > 0 ||
|
||||
activeDbTypes.length > 0 ||
|
||||
(activeEnvironment !== null && activeEnvironment !== undefined);
|
||||
|
||||
const connectionMatchesFilters = (c: Connection) => {
|
||||
if (activeTagIds.length > 0 && !c.tag_ids.some((id) => activeTagIds.includes(id))) {
|
||||
return false;
|
||||
}
|
||||
if (activeDbTypes.length > 0 && !activeDbTypes.includes(c.db_type)) {
|
||||
return false;
|
||||
}
|
||||
if (activeEnvironment !== null && activeEnvironment !== undefined && c.environment !== activeEnvironment) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const visibleFolders = hasSearch
|
||||
? []
|
||||
: getChildFolders(folders, currentFolderId).filter((f) => {
|
||||
if (!hasActiveFilters) return true;
|
||||
const folderMatchesTags = f.tag_ids.some((id) => activeTagIds.includes(id));
|
||||
if (folderMatchesTags) return true;
|
||||
const subIds = new Set(getDescendantFolderIds(folders, f.id));
|
||||
return connections.some(
|
||||
(c) => c.folder_id !== null && subIds.has(c.folder_id) && connectionMatchesFilters(c),
|
||||
);
|
||||
});
|
||||
const directConnections = hasSearch
|
||||
? connections
|
||||
: connections.filter((c) => c.folder_id === currentFolderId);
|
||||
const allStoreConnections = useConnectionStore((s) => s.connections);
|
||||
const allStoreFolders = useConnectionStore((s) => s.folders);
|
||||
// Check if any direct connections OR any subfolder has connections anywhere below
|
||||
const hasItems = visibleFolders.length > 0 || directConnections.length > 0 ||
|
||||
(currentFolderId && allStoreConnections.some((c) => {
|
||||
const allowed = new Set(getDescendantFolderIds(allStoreFolders, currentFolderId));
|
||||
return c.folder_id !== null && allowed.has(c.folder_id);
|
||||
}));
|
||||
const isSelecting = selectedItemIds.length > 0;
|
||||
const activeFolder = currentFolderId
|
||||
? (folders.find((f) => f.id === currentFolderId) ?? null)
|
||||
: null;
|
||||
|
||||
const handleFolderClick = (folderId: string) => {
|
||||
if (isSelecting) {
|
||||
toggleItemSelection(folderId);
|
||||
} else {
|
||||
onFolderSelect?.(folderId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBreadcrumbNavigate = (folderId: string | null) => {
|
||||
clearSelection();
|
||||
onFolderSelect?.(folderId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<FolderBreadcrumb
|
||||
folders={folders}
|
||||
activeFolderId={currentFolderId}
|
||||
onNavigate={handleBreadcrumbNavigate}
|
||||
hasSearch={hasSearch}
|
||||
onClearSearch={() => useUiStore.getState().clearFilters()}
|
||||
/>
|
||||
{activeFolder && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => onEditFolder?.(activeFolder)}
|
||||
className="inline-flex items-center gap-1 text-xs text-text-muted hover:text-text transition-colors px-2 py-1 rounded-md cursor-pointer"
|
||||
>
|
||||
<Pencil size={12} /> Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDeleteFolder?.(activeFolder)}
|
||||
className="inline-flex items-center gap-1 text-xs !text-red-400 hover:!text-red-300 transition-colors px-2 py-1 rounded-md cursor-pointer"
|
||||
>
|
||||
<Trash2 size={12} /> Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hasItems ? (
|
||||
<div className="text-center w-full py-16 text-text-muted">
|
||||
{hasSearch
|
||||
? "No connections match your search."
|
||||
: activeFolderId
|
||||
? "This folder is empty. Add a connection or subfolder."
|
||||
: "No connections yet. Create one to get started."}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="grid gap-3"
|
||||
style={{
|
||||
gridTemplateColumns:
|
||||
"repeat(auto-fill, minmax(260px, 1fr))",
|
||||
}}
|
||||
>
|
||||
{visibleFolders.map((f) => {
|
||||
const isSelected = selectedItemIds.includes(f.id);
|
||||
// Count all connections in this subfolder (including nested descendants)
|
||||
const subIds = new Set(getDescendantFolderIds(folders, f.id));
|
||||
const count = allStoreConnections.filter(
|
||||
(c) => c.folder_id !== null && subIds.has(c.folder_id),
|
||||
).length;
|
||||
const subfolderCount = getChildFolders(
|
||||
folders,
|
||||
f.id,
|
||||
).length;
|
||||
const tagMap = new Map(tags.map((t) => [t.id, t]));
|
||||
const folderTags = f.tag_ids
|
||||
.map((id) => tagMap.get(id))
|
||||
.filter(Boolean) as Tag[];
|
||||
return (
|
||||
<DroppableFolderCard
|
||||
key={f.id}
|
||||
folder={f}
|
||||
isSelected={isSelected}
|
||||
count={count}
|
||||
subfolderCount={subfolderCount}
|
||||
folderTags={folderTags}
|
||||
onFolderClick={handleFolderClick}
|
||||
onToggleSelection={toggleItemSelection}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{directConnections.map((c) => (
|
||||
<ConnectionCard
|
||||
key={c.id}
|
||||
connection={c}
|
||||
tags={tags}
|
||||
onTagToggle={onTagToggle}
|
||||
onOpenDbViewer={onOpenDbViewer}
|
||||
onEdit={onEditConnection}
|
||||
onDuplicate={onDuplicateConnection}
|
||||
onDelete={onDeleteConnection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user