feat: rebuild desktop sync as linked server accounts, fix packaged plugin bundles and UI scaling

This commit is contained in:
LukeGus committed 2026-09-26 00:41:25 -05:00
1 parent 7c02daccf4
commit 6cd9a2846d
242 files changed
+39127 -8558

No files matched your search

+16 -11
View File
@@ -15,6 +15,7 @@ import { Skeleton } from "@/components/skeleton";
import { readRailPreference, setRailPreference } from "./rail-preferences";
import { useRailItems, type RailItemDef } from "./rail-items";
import { RailBadge } from "./RailBadge";
import { rem } from "@/lib/rem";
/** Core rail views; plugins add their own ids at runtime. */
export type CoreRailView =
@@ -102,7 +103,7 @@ function buildRailButtons(
const btnBase =
"relative flex items-center h-7 rounded shrink-0 transition-colors gap-2.5";
const btnStyle = { margin: "0 4px", padding: "0 8px" };
const btnStyle = { margin: `0 ${rem(4)}`, padding: `0 ${rem(8)}` };
export function AppRail({
railView,
@@ -240,7 +241,7 @@ export function AppRail({
return (
<div
className="hidden md:flex flex-col items-stretch bg-sidebar border-r border-border shrink-0 overflow-hidden pt-2 gap-1 transition-[width] duration-200 min-h-0"
style={{ width: railExpanded ? 160 : 40 }}
style={{ width: rem(railExpanded ? 160 : 40) }}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onContextMenu={(e) => {
@@ -273,7 +274,9 @@ export function AppRail({
<div
key={`sep-${i}`}
className="mx-auto h-px bg-border my-0.5 shrink-0 transition-[width] duration-200"
style={{ width: railExpanded ? "calc(100% - 16px)" : 20 }}
style={{
width: railExpanded ? `calc(100% - ${rem(16)})` : rem(20),
}}
/>
) : "tabType" in item ? (
<button
@@ -284,7 +287,7 @@ export function AppRail({
>
<span
className="relative shrink-0 flex items-center justify-center"
style={{ width: 16, height: 16 }}
style={{ width: rem(16), height: rem(16) }}
>
{item.icon}
{item.useBadge && (
@@ -343,7 +346,7 @@ export function AppRail({
>
<span
className="relative shrink-0 flex items-center justify-center"
style={{ width: 16, height: 16 }}
style={{ width: rem(16), height: rem(16) }}
>
{item.icon}
{item.useBadge && (
@@ -384,7 +387,7 @@ export function AppRail({
>
<span
className="shrink-0 flex items-center justify-center"
style={{ width: 16, height: 16 }}
style={{ width: rem(16), height: rem(16) }}
>
<Pin size={16} />
</span>
@@ -400,7 +403,9 @@ export function AppRail({
{showPinButton && (
<div
className="mx-auto h-px bg-border my-0.5 shrink-0 transition-[width] duration-200"
style={{ width: railExpanded ? "calc(100% - 16px)" : 20 }}
style={{
width: railExpanded ? `calc(100% - ${rem(16)})` : rem(20),
}}
/>
)}
{footerItems.map((item) => {
@@ -449,7 +454,7 @@ export function AppRail({
>
<span
className="relative shrink-0 flex items-center justify-center"
style={{ width: 16, height: 16 }}
style={{ width: rem(16), height: rem(16) }}
>
<Icon size={16} />
{item.useBadge && (
@@ -528,7 +533,7 @@ export function AppRail({
>
<span
className="relative shrink-0 flex items-center justify-center"
style={{ width: 16, height: 16 }}
style={{ width: rem(16), height: rem(16) }}
>
{item.icon}
</span>
@@ -549,7 +554,7 @@ export function AppRail({
>
<span
className="shrink-0 flex items-center justify-center"
style={{ width: 16, height: 16 }}
style={{ width: rem(16), height: rem(16) }}
>
<LogOut size={16} />
</span>
@@ -571,7 +576,7 @@ export function AppRail({
>
<div
className="rounded-full bg-accent-brand/20 border border-accent-brand/30 flex items-center justify-center font-bold text-accent-brand shrink-0"
style={{ width: 24, height: 24, fontSize: 11 }}
style={{ width: rem(24), height: rem(24), fontSize: rem(11) }}
>
{username.charAt(0).toUpperCase() || "U"}
</div>
+16
View File
@@ -19,12 +19,15 @@ import {
} from "@/components/folder-style";
import { normalizePath, splitPath } from "./FolderPathPicker";
import { getCredentials } from "@/main-axios";
import { useSyncStatus } from "@/hooks/use-sync-status";
import { FakeSwitch, SettingRow } from "@/components/section-card";
export type FolderMetadataValue = {
name: string;
color: string;
icon: string;
credentialId: number | null;
localOnly: boolean;
};
type CredentialOption = { id: string; name: string; username?: string };
@@ -45,6 +48,7 @@ export function FolderMetadataDialog({
color?: string;
icon?: string;
credentialId?: number | null;
localOnly?: boolean;
};
/** Every folder path that already exists, for duplicate-name validation. */
existingPaths?: string[];
@@ -58,6 +62,8 @@ export function FolderMetadataDialog({
const [color, setColor] = useState(DEFAULT_FOLDER_COLOR);
const [icon, setIcon] = useState(DEFAULT_FOLDER_ICON);
const [credentialId, setCredentialId] = useState<string>("");
const [localOnly, setLocalOnly] = useState(false);
const syncLinked = !!useSyncStatus()?.linked;
const [credentials, setCredentials] = useState<CredentialOption[]>([]);
const parentPath =
@@ -84,6 +90,7 @@ export function FolderMetadataDialog({
setCredentialId(
initial?.credentialId ? String(initial.credentialId) : "",
);
setLocalOnly(!!initial?.localOnly);
}
}, [open, initial]);
@@ -111,6 +118,7 @@ export function FolderMetadataDialog({
color,
icon,
credentialId: credentialId ? Number(credentialId) : null,
localOnly,
});
onOpenChange(false);
}
@@ -186,6 +194,14 @@ export function FolderMetadataDialog({
{t("hosts.folderCredentialHint")}
</p>
</div>
{syncLinked && (
<SettingRow
label={t("hosts.folderLocalOnly")}
description={t("hosts.folderLocalOnlyDesc")}
>
<FakeSwitch checked={localOnly} onChange={setLocalOnly} />
</SettingRow>
)}
<div className="flex flex-col gap-1.5">
<label className="text-xs font-semibold">
{t("hosts.folderPreview")}
+13 -17
View File
@@ -45,7 +45,9 @@ export function HostAuthOverrideModal({
const ownerAuthShared =
overrideState?.ownerAuthShared ??
(protocol === "ssh" ? !!host.shareSshAuth : false);
const remoteShared = !!host.isShared && Number(host.id) < 0;
// A shared host's copy on a linked desktop: its override lives on the
// server, against the server's own credentials.
const sharedCopySyncId = host.sharedCopy ? (host.syncId ?? null) : null;
useEffect(() => {
if (!open) return;
@@ -53,18 +55,16 @@ export function HostAuthOverrideModal({
setLoading(true);
setLoadError(false);
const credentialsRequest = remoteShared
const credentialsRequest = sharedCopySyncId
? getConnectedRemoteApi().then((api) => {
if (!api) throw new Error("Remote server is not connected");
if (!api) throw new Error("The linked server is not reachable");
return api.get("/credentials").then((response) => response.data);
})
: getCredentials();
Promise.all([
credentialsRequest,
remoteShared
? getHostAuthOverride(Number(host.id), protocol, true)
: getHostAuthOverride(Number(host.id), protocol),
getHostAuthOverride(Number(host.id), protocol, sharedCopySyncId),
])
.then(([credentialResult, overrideResult]) => {
if (cancelled) return;
@@ -87,22 +87,18 @@ export function HostAuthOverrideModal({
return () => {
cancelled = true;
};
}, [host.id, open, protocol, remoteShared]);
}, [host.id, open, protocol, sharedCopySyncId]);
const handleSave = async () => {
setSaving(true);
try {
const credentialId = selectedId ? Number(selectedId) : null;
if (remoteShared) {
await setHostAuthOverride(
Number(host.id),
protocol,
credentialId,
true,
);
} else {
await setHostAuthOverride(Number(host.id), protocol, credentialId);
}
await setHostAuthOverride(
Number(host.id),
protocol,
credentialId,
sharedCopySyncId,
);
toast.success(
credentialId === null
? t(
+2
View File
@@ -167,6 +167,7 @@ export function createHostEditorForm(
socks5ProxyChain: (host?.socks5ProxyChain ?? []) as HostSocks5ProxyNode[],
connectionOrigin: (host?.connectionOrigin ?? null) as
"local" | "remote" | null,
localOnly: host?.localOnly ?? false,
forceKeyboardInteractive: host?.forceKeyboardInteractive ?? false,
inheritTerminalAppearance:
!host ||
@@ -415,6 +416,7 @@ export function buildHostEditorPayload(
socks5ProxyChain:
form.socks5ProxyMode === "chain" ? form.socks5ProxyChain : null,
connectionOrigin: form.connectionOrigin,
localOnly: form.localOnly,
enableSsh: protocols.enableSsh,
sshPort: Number(form.sshPort),
forceKeyboardInteractive: form.forceKeyboardInteractive,
+13
View File
@@ -11,6 +11,7 @@ import { FolderPathPicker } from "./FolderPathPicker";
import { HostParentPicker } from "./HostParentPicker";
import { getSSHFolders, isElectron } from "@/main-axios";
import { connectionOriginAppliesTo } from "./HostEditorData";
import { useSyncStatus } from "@/hooks/use-sync-status";
import type { HostEditorForm, HostProtocols } from "./HostEditorData";
import { Select2 } from "@/components/select2";
import { useHostProtocols } from "./host-protocols";
@@ -39,6 +40,7 @@ export function HostEditorGeneralTab({
simpleMode?: boolean;
}) {
const { t } = useTranslation();
const syncLinked = !!useSyncStatus()?.linked;
const pluginProtocols = useHostProtocols();
// Tracks which picker is shown, independent of whether a value is set yet
@@ -311,6 +313,17 @@ export function HostEditorGeneralTab({
onChange={(v) => setField("pin", v)}
/>
</SettingRow>
{syncLinked && (
<SettingRow
label={t("hosts.localOnly")}
description={t("hosts.localOnlyDesc")}
>
<FakeSwitch
checked={form.localOnly}
onChange={(v) => setField("localOnly", v)}
/>
</SettingRow>
)}
</div>
<div className="flex flex-col gap-3 border-t border-border pt-4 pb-0">
<div className="flex items-center justify-between">
+2
View File
@@ -78,6 +78,8 @@ export function sshHostToHost(h: SSHHostWithStatus): Host {
pin: h.pin ?? false,
sortOrder: h.sortOrder ?? null,
connectionOrigin: h.connectionOrigin ?? null,
localOnly: !!h.localOnly,
sharedCopy: !!h.sharedCopy,
enableSsh: h.enableSsh != null ? h.enableSsh : isSshHost,
sshPort:
h.sshPort ??
+83 -103
View File
@@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
import { copyToClipboard } from "@/lib/clipboard";
import {
getUserInfo,
getRemoteSyncUserInfo,
getApiKeys,
createApiKey,
deleteApiKey,
@@ -20,11 +19,10 @@ import {
import { getDatabaseTransferUrl } from "@/lib/database-transfer-url";
import { readRailPreference, setRailPreference } from "./rail-preferences";
import { useHostSidebarPreferences } from "./tree/hooks/useHostSidebarPreferences";
import type { UserRole } from "@/main-axios";
import type { LinkedAccountInfo, UserRole } from "@/main-axios";
import type React from "react";
import { isElectron } from "@/lib/electron";
import { RemoteSyncPanel } from "@/settings/RemoteSyncPanel.tsx";
import { shouldForceLocalPreferenceStorage } from "@/settings/remote-sync-state";
import { shell } from "@/plugin-host/shell-bridge";
import { Button } from "@/components/button";
import { Input } from "@/components/input";
import { VersionBadge } from "@/components/version-badge";
@@ -522,15 +520,9 @@ export function UserProfilePanel({
onLogout,
userPrefs,
onPrefsChange,
remoteSyncInitialServerUrl,
remoteSyncReconnectRequested,
onRemoteSyncReconnectHandled,
}: {
username?: string;
onLogout?: () => void;
remoteSyncInitialServerUrl?: string;
remoteSyncReconnectRequested?: boolean;
onRemoteSyncReconnectHandled?: () => void;
userPrefs?: {
reopenTabsOnLogin: boolean;
storageMode?: string | null;
@@ -629,51 +621,19 @@ export function UserProfilePanel({
}
}, [userPrefs?.storageMode]);
// Remote sync is not connected by default on the desktop app (it's an
// opt-in feature configured from this same panel). "cloud" storage mode
// and Termix ID both assume a real multi-device server account, so they
// stay hidden/forced-off until the user actually connects one.
const [isRemoteSyncConnected, setIsRemoteSyncConnected] = useState<
boolean | null
>(() => (isElectron() ? null : true));
// On the desktop app, the account it is linked to on a server, if any.
// "Cloud" storage only has somewhere to go once it is linked.
const [linkedAccount, setLinkedAccount] = useState<
LinkedAccountInfo | null | undefined
>(() => (isElectron() ? undefined : null));
const isLinked = !!linkedAccount;
useEffect(() => {
if (!isElectron()) return;
let cancelled = false;
const refreshSyncStatus = () => {
window.electronAPI
?.invoke?.("get-remote-sync-config")
.then((config) => {
if (!cancelled) {
setIsRemoteSyncConnected(
!!(config as { serverUrl?: string } | null)?.serverUrl,
);
}
})
.catch(() => {});
};
refreshSyncStatus();
const unsubscribe = window.electronAPI?.onRemoteSyncStatusChanged?.(() =>
refreshSyncStatus(),
);
return () => {
cancelled = true;
unsubscribe?.();
};
}, []);
useEffect(() => {
if (
shouldForceLocalPreferenceStorage(
isElectron(),
isRemoteSyncConnected,
storageMode,
)
) {
if (isElectron() && linkedAccount === null && storageMode === "cloud") {
setStorageMode("local");
onPrefsChange?.({ storageMode: "local" });
}
}, [isRemoteSyncConnected, storageMode, onPrefsChange]);
}, [linkedAccount, storageMode, onPrefsChange]);
// Settings toggles — all backed by localStorage
const [commandAutocomplete, setCommandAutocomplete] = useState(
@@ -738,16 +698,17 @@ export function UserProfilePanel({
useEffect(() => {
getUserInfo()
.then(async (localInfo) => {
setUserId(localInfo.userId);
setIsOidc(localInfo.is_oidc ?? false);
setIsDualAuth(localInfo.is_dual_auth ?? false);
const remoteInfo = await getRemoteSyncUserInfo();
const info = remoteInfo ?? localInfo;
setAccountUsername(info.username);
.then((info) => {
setUserId(info.userId);
setIsOidc(info.is_oidc ?? false);
setIsDualAuth(info.is_dual_auth ?? false);
const linked = info.linked ?? null;
setLinkedAccount(linked);
// A linked desktop is the account it is signed in to on the server.
setAccountUsername(linked?.username || info.username);
setAccountTotpEnabled(info.totp_enabled ?? false);
setUserRole(
info.is_admin
(linked ? linked.isAdmin : info.is_admin)
? t("newUi.sidebar.userProfile.roleAdministrator")
: t("newUi.sidebar.userProfile.roleUser"),
);
@@ -758,10 +719,17 @@ export function UserProfilePanel({
} else {
setAuthMethod(t("newUi.sidebar.userProfile.authMethodLocal"));
}
if (remoteInfo) {
setUserRoles(remoteInfo.roles ?? []);
if (linked) {
setUserRoles(
linked.roles.map((name, index) => ({
id: index,
roleId: index,
roleName: name,
roleDisplayName: name,
})) as unknown as UserRole[],
);
} else {
getUserRoles(localInfo.userId)
getUserRoles(info.userId)
.then(({ roles }) => setUserRoles(roles ?? []))
.catch(() => {});
}
@@ -1340,7 +1308,7 @@ export function UserProfilePanel({
{/* Storage mode toggle — only meaningful once a remote server is
connected; with no sync there's nowhere for "cloud" to sync to,
so this stays forced to local storage and hidden. */}
{(!isElectron() || isRemoteSyncConnected === true) && (
{(!isElectron() || isLinked) && (
<div className="border border-border bg-card px-3 py-2.5 flex flex-col gap-2">
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-foreground">
{t("newUi.sidebar.userProfile.storageModeSwitch")}
@@ -1389,17 +1357,35 @@ export function UserProfilePanel({
onToggle={() => toggle("account")}
>
<div className="flex flex-col gap-0 pt-2">
{isElectron() ? (
{isElectron() && (
<div className="border border-accent-brand/40 bg-accent-brand/10 px-3 py-2.5 mb-2">
<div className="flex items-center gap-1.5 text-xs font-semibold text-accent-brand">
<ShieldCheck className="size-3.5" />
{t("newUi.sidebar.userProfile.desktopProfileTitle")}
{isLinked
? t("newUi.sidebar.userProfile.linkedProfileTitle", {
server:
linkedAccount?.serverName || linkedAccount?.serverUrl,
})
: t("newUi.sidebar.userProfile.desktopProfileTitle")}
</div>
<p className="text-[10px] text-muted-foreground leading-relaxed mt-1">
{t("newUi.sidebar.userProfile.desktopProfileDescription")}
{isLinked
? t("newUi.sidebar.userProfile.linkedProfileDescription")
: t("newUi.sidebar.userProfile.desktopProfileDescription")}
</p>
<Button
variant="outline"
size="sm"
className="mt-2 h-7 text-[10px] rounded-none"
onClick={() => shell.openRailView("sync")}
>
{isLinked
? t("newUi.sidebar.userProfile.manageSync")
: t("newUi.sidebar.userProfile.setUpSync")}
</Button>
</div>
) : (
)}
{(!isElectron() || isLinked) && (
<div className="grid grid-cols-2 gap-x-4 gap-y-0">
<div className="flex flex-col py-2">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold">
@@ -1427,33 +1413,37 @@ export function UserProfilePanel({
))}
</div>
</div>
<div className="flex flex-col py-2">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold">
{t("newUi.sidebar.userProfile.authMethodLabel")}
</span>
<span className="text-sm font-semibold mt-0.5">
{authMethod || "—"}
</span>
</div>
<div className="flex flex-col py-2">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold">
{t("newUi.sidebar.userProfile.twoFaLabel")}
</span>
<span className="flex items-center gap-1 mt-0.5">
{accountTotpEnabled ? (
<>
<ShieldCheck className="size-3.5 text-accent-brand" />
<span className="text-sm font-semibold text-accent-brand">
{t("newUi.sidebar.userProfile.twoFaOn")}
</span>
</>
) : (
<span className="text-sm font-semibold text-muted-foreground">
{t("newUi.sidebar.userProfile.twoFaOff")}
{!isElectron() && (
<>
<div className="flex flex-col py-2">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold">
{t("newUi.sidebar.userProfile.authMethodLabel")}
</span>
)}
</span>
</div>
<span className="text-sm font-semibold mt-0.5">
{authMethod || "—"}
</span>
</div>
<div className="flex flex-col py-2">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold">
{t("newUi.sidebar.userProfile.twoFaLabel")}
</span>
<span className="flex items-center gap-1 mt-0.5">
{accountTotpEnabled ? (
<>
<ShieldCheck className="size-3.5 text-accent-brand" />
<span className="text-sm font-semibold text-accent-brand">
{t("newUi.sidebar.userProfile.twoFaOn")}
</span>
</>
) : (
<span className="text-sm font-semibold text-muted-foreground">
{t("newUi.sidebar.userProfile.twoFaOff")}
</span>
)}
</span>
</div>
</>
)}
</div>
)}
@@ -1519,16 +1509,6 @@ export function UserProfilePanel({
</div>
</div>
{isElectron() && (
<div className="border-t border-border pt-3 mt-3">
<RemoteSyncPanel
initialServerUrl={remoteSyncInitialServerUrl}
reconnectRequested={remoteSyncReconnectRequested}
onReconnectRequestHandled={onRemoteSyncReconnectHandled}
/>
</div>
)}
{!isElectron() && (
<div className="border-t border-border pt-3 mt-3">
<div className="flex items-center justify-between">
@@ -2011,7 +1991,7 @@ export function UserProfilePanel({
{/* The embedded desktop backend auto-authenticates its machine-local
profile, so server login controls would imply protection they do
not provide. Remote Sync owns its separate account UI above. */}
not provide. A linked account's sign-in is managed on its server. */}
<AccordionSection
hidden={isElectron()}
id="security"
+2
View File
@@ -27,6 +27,7 @@ export function buildHostTree(
icon?: string;
credentialId?: number | null;
sortOrder?: number | null;
localOnly?: boolean;
}
>,
): HostFolder {
@@ -48,6 +49,7 @@ export function buildHostTree(
icon: meta?.icon,
credentialId: meta?.credentialId ?? null,
sortOrder: meta?.sortOrder ?? null,
localOnly: !!meta?.localOnly,
children: [],
};
folderMap.set(accumulated, folder);
@@ -1,3 +1,4 @@
import { rem } from "@/lib/rem";
import { useTranslation } from "react-i18next";
import {
ChevronRight,
@@ -114,7 +115,7 @@ export function CredentialItem({
const canDrag = arrangeMode && !isTouchOnly;
const depthStyle =
depth > 0 ? ({ paddingLeft: depth * 12 } as const) : undefined;
depth > 0 ? ({ paddingLeft: rem(depth * 12) } as const) : undefined;
const trayButtonClass =
"flex items-center justify-center size-6.5 text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors";
@@ -1,3 +1,4 @@
import { rem } from "@/lib/rem";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
@@ -64,7 +65,7 @@ export function CredentialFolderItem({
return (
<div
className="relative"
style={depth > 0 ? { paddingLeft: depth * 12 } : undefined}
style={depth > 0 ? { paddingLeft: rem(depth * 12) } : undefined}
>
<div
onDragOver={(e) => {
+10
View File
@@ -1,5 +1,6 @@
import {
Braces,
Cloud,
Hammer,
KeyRound,
LayoutPanelLeft,
@@ -12,6 +13,7 @@ import {
} from "lucide-react";
import { useMemo, useSyncExternalStore } from "react";
import { usePermissions } from "@/hooks/use-permissions";
import { useSyncAttentionCount } from "@/hooks/use-sync-status";
import { isElectron } from "@/lib/electron";
import { createRegistry } from "@/lib/registry";
@@ -116,6 +118,14 @@ export const RAIL_ITEMS: RailItemDef[] = [
labelKey: "nav.splitScreen",
separatorAfter: true,
},
{
id: "sync",
icon: Cloud,
labelKey: "nav.sync",
electronOnly: true,
placement: "footer",
useBadge: useSyncAttentionCount,
},
];
/**
@@ -1,4 +1,5 @@
/* eslint-disable react-refresh/only-export-components */
import { rem } from "@/lib/rem";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ChevronRight, Check, GripVertical } from "lucide-react";
@@ -177,7 +178,7 @@ export function FolderItem({
return (
<div
className="relative"
style={depth > 0 ? { paddingLeft: depth * 12 } : undefined}
style={depth > 0 ? { paddingLeft: rem(depth * 12) } : undefined}
>
<div className="relative">
<button
+2 -1
View File
@@ -1,4 +1,5 @@
/* eslint-disable react-refresh/only-export-components */
import { rem } from "@/lib/rem";
import { enabledHostProtocols } from "@/sidebar/host-protocols";
import { useEffect, useRef, useState, type MouseEvent } from "react";
import { createPortal } from "react-dom";
@@ -368,7 +369,7 @@ export function HostItem({
if (query && !hostMatchesQuery(host, query)) return null;
const depthStyle =
depth > 0 ? ({ paddingLeft: depth * 12 } as const) : undefined;
depth > 0 ? ({ paddingLeft: rem(depth * 12) } as const) : undefined;
const trayButtonClass =
"flex items-center justify-center size-[22.75px] text-muted-foreground/60 hover:text-foreground hover:bg-muted transition-colors";
+4
View File
@@ -459,6 +459,7 @@ export function SidebarTree({
color: string;
icon: string;
credentialId: number | null;
localOnly: boolean;
}) {
const existing = folderDialog?.folder;
try {
@@ -476,6 +477,7 @@ export function SidebarTree({
value.color,
value.icon,
value.credentialId,
value.localOnly,
);
} else {
await updateFolderMetadata(
@@ -483,6 +485,7 @@ export function SidebarTree({
value.color,
value.icon,
value.credentialId,
value.localOnly,
);
}
window.dispatchEvent(new CustomEvent("termix:hosts-changed"));
@@ -1346,6 +1349,7 @@ export function SidebarTree({
color: folderDialog.folder.color,
icon: folderDialog.folder.icon,
credentialId: folderDialog.folder.credentialId,
localOnly: folderDialog.folder.localOnly,
}
: undefined
}