Add plugin framework and settings UI
This commit is contained in:
@@ -29,6 +29,37 @@ import { cn } from "../lib/utils";
|
||||
import { NotFoundPage } from "../pages/NotFound";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
const INSTANCE_SETTINGS_MEMORY_KEY = "paperclip.lastInstanceSettingsPath";
|
||||
const DEFAULT_INSTANCE_SETTINGS_PATH = "/instance/settings/heartbeats";
|
||||
|
||||
function normalizeRememberedInstanceSettingsPath(rawPath: string | null): string {
|
||||
if (!rawPath) return DEFAULT_INSTANCE_SETTINGS_PATH;
|
||||
|
||||
const match = rawPath.match(/^([^?#]*)(\?[^#]*)?(#.*)?$/);
|
||||
const pathname = match?.[1] ?? rawPath;
|
||||
const search = match?.[2] ?? "";
|
||||
const hash = match?.[3] ?? "";
|
||||
|
||||
if (pathname === "/instance/settings/heartbeats" || pathname === "/instance/settings/plugins") {
|
||||
return `${pathname}${search}${hash}`;
|
||||
}
|
||||
|
||||
if (/^\/instance\/settings\/plugins\/[^/?#]+$/.test(pathname)) {
|
||||
return `${pathname}${search}${hash}`;
|
||||
}
|
||||
|
||||
return DEFAULT_INSTANCE_SETTINGS_PATH;
|
||||
}
|
||||
|
||||
function readRememberedInstanceSettingsPath(): string {
|
||||
if (typeof window === "undefined") return DEFAULT_INSTANCE_SETTINGS_PATH;
|
||||
try {
|
||||
return normalizeRememberedInstanceSettingsPath(window.localStorage.getItem(INSTANCE_SETTINGS_MEMORY_KEY));
|
||||
} catch {
|
||||
return DEFAULT_INSTANCE_SETTINGS_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
export function Layout() {
|
||||
const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar();
|
||||
const { openNewIssue, openOnboarding } = useDialog();
|
||||
@@ -49,6 +80,7 @@ export function Layout() {
|
||||
const onboardingTriggered = useRef(false);
|
||||
const lastMainScrollTop = useRef(0);
|
||||
const [mobileNavVisible, setMobileNavVisible] = useState(true);
|
||||
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
|
||||
const nextTheme = theme === "dark" ? "light" : "dark";
|
||||
const matchedCompany = useMemo(() => {
|
||||
if (!companyPrefix) return null;
|
||||
@@ -220,6 +252,21 @@ export function Layout() {
|
||||
};
|
||||
}, [isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!location.pathname.startsWith("/instance/settings/")) return;
|
||||
|
||||
const nextPath = normalizeRememberedInstanceSettingsPath(
|
||||
`${location.pathname}${location.search}${location.hash}`,
|
||||
);
|
||||
setInstanceSettingsTarget(nextPath);
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(INSTANCE_SETTINGS_MEMORY_KEY, nextPath);
|
||||
} catch {
|
||||
// Ignore storage failures in restricted environments.
|
||||
}
|
||||
}, [location.hash, location.pathname, location.search]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -235,7 +282,6 @@ export function Layout() {
|
||||
</a>
|
||||
<WorktreeBanner />
|
||||
<div className={cn("min-h-0 flex-1", isMobile ? "w-full" : "flex overflow-hidden")}>
|
||||
{/* Mobile backdrop */}
|
||||
{isMobile && sidebarOpen && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -245,7 +291,6 @@ export function Layout() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Combined sidebar area: company rail + inner sidebar + docs bar */}
|
||||
{isMobile ? (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -270,7 +315,7 @@ export function Layout() {
|
||||
</a>
|
||||
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
|
||||
<Link
|
||||
to="/instance/settings"
|
||||
to={instanceSettingsTarget}
|
||||
aria-label="Instance settings"
|
||||
title="Instance settings"
|
||||
onClick={() => {
|
||||
@@ -320,7 +365,7 @@ export function Layout() {
|
||||
</a>
|
||||
<Button variant="ghost" size="icon-sm" className="text-muted-foreground shrink-0" asChild>
|
||||
<Link
|
||||
to="/instance/settings"
|
||||
to={instanceSettingsTarget}
|
||||
aria-label="Instance settings"
|
||||
title="Instance settings"
|
||||
onClick={() => {
|
||||
@@ -346,7 +391,6 @@ export function Layout() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main content */}
|
||||
<div className={cn("flex min-w-0 flex-col", isMobile ? "w-full" : "h-full flex-1")}>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
Reference in New Issue
Block a user