feat(FN-1493): add directory browsing UX to plugin install flow

- Add DirectoryPicker component for browsing filesystem to select plugin source
- Add dist-folder and package-root install-source resolution in backend
- Wire frontend installPlugin to mode-discriminated endpoint (/plugins/install-local)
- Add comprehensive regression tests for browse-driven plugin install workflow
- Update PluginManager UI with directory picker integration and CSS styles
This commit is contained in:
gsxdsm
2026-04-12 18:10:39 -07:00
parent 9d2f61501b
commit 717124fc9e
10 changed files with 1318 additions and 119 deletions

View File

@@ -13,6 +13,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { Package, Settings, Trash2, Plus, X, RefreshCw, RotateCcw } from "lucide-react";
import { fetchPlugins, installPlugin, enablePlugin, disablePlugin, uninstallPlugin, fetchPluginSettings, updatePluginSettings, reloadPlugin } from "../api";
import { DirectoryPicker } from "./DirectoryPicker";
import type { PluginInstallation, PluginState } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
@@ -429,18 +430,25 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
{showInstall && (
<div className="plugin-install-form">
<input
type="text"
placeholder="Local path to plugin directory"
<p className="plugin-install-hint">
Browse to a plugin package root (contains <code>manifest.json</code>) or a built <code>dist</code> directory.
</p>
<DirectoryPicker
value={installPath}
onChange={(e) => setInstallPath(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleInstall()}
onChange={setInstallPath}
placeholder="Absolute path to plugin directory or dist folder"
onInputKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleInstall();
}
}}
/>
<div className="plugin-install-actions">
<button className="btn-primary" onClick={handleInstall} disabled={installing}>
{installing ? "Installing..." : "Install"}
<button className="btn-primary" onClick={handleInstall} disabled={installing || !installPath.trim()}>
{installing ? "Installing..." : "Install Plugin"}
</button>
<button className="btn-secondary" onClick={() => setShowInstall(false)}>
<button className="btn-secondary" onClick={() => { setShowInstall(false); setInstallPath(""); }}>
Cancel
</button>
</div>