refactor: Vue 3 state, TypeScript, and lifecycle improvements
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m49s

- connectionsByGroup memoized as computed map — eliminates redundant filter on every render
- DockerPanel: replace any[] with typed DockerContainer/Image/Volume interfaces
- useRdp: replace ReturnType<typeof ref<boolean>> with Ref<boolean>
- SettingsModal: debounce sidebarWidth slider watch (300ms) to prevent rapid IPC calls
- useSftp: export cleanupSession() to prevent sessionPaths memory leak
- StatusBar, CommandPalette: migrate defineEmits to Vue 3.3+ tuple syntax
- SidebarToggle: replace manual v-model (defineProps + defineEmits) with defineModel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell 2026-03-29 16:53:17 -04:00
parent ebd3cee49e
commit 28619bba3f
8 changed files with 53 additions and 30 deletions

View File

@ -116,9 +116,9 @@ const connectionStore = useConnectionStore();
const sessionStore = useSessionStore();
const emit = defineEmits<{
(e: "open-import"): void;
(e: "open-settings"): void;
(e: "open-new-connection", protocol?: "ssh" | "rdp"): void;
"open-import": [];
"open-settings": [];
"open-new-connection": [protocol?: "ssh" | "rdp"];
}>();
const actions: PaletteAction[] = [

View File

@ -422,9 +422,16 @@ watch(
() => settings.value.defaultProtocol,
(val) => invoke("set_setting", { key: "default_protocol", value: val }).catch(console.error),
);
let sidebarWidthDebounce: ReturnType<typeof setTimeout>;
watch(
() => settings.value.sidebarWidth,
(val) => invoke("set_setting", { key: "sidebar_width", value: String(val) }).catch(console.error),
(val) => {
clearTimeout(sidebarWidthDebounce);
sidebarWidthDebounce = setTimeout(
() => invoke("set_setting", { key: "sidebar_width", value: String(val) }).catch(console.error),
300,
);
},
);
watch(
() => settings.value.terminalTheme,

View File

@ -47,7 +47,7 @@ const connectionStore = useConnectionStore();
const activeThemeName = ref("Default");
const emit = defineEmits<{
(e: "open-theme-picker"): void;
"open-theme-picker": [];
}>();
const connectionInfo = computed(() => {

View File

@ -5,11 +5,11 @@
:key="tab.id"
class="flex-1 py-2 text-xs font-medium text-center transition-colors cursor-pointer"
:class="
modelValue === tab.id
model === tab.id
? 'text-[var(--wraith-accent-blue)] border-b-2 border-[var(--wraith-accent-blue)]'
: 'text-[var(--wraith-text-muted)] hover:text-[var(--wraith-text-secondary)]'
"
@click="emit('update:modelValue', tab.id)"
@click="model = tab.id"
>
{{ tab.label }}
</button>
@ -24,11 +24,5 @@ const tabs = [
{ id: "sftp" as const, label: "SFTP" },
];
defineProps<{
modelValue: SidebarTab;
}>();
const emit = defineEmits<{
"update:modelValue": [tab: SidebarTab];
}>();
const model = defineModel<SidebarTab>();
</script>

View File

@ -81,12 +81,16 @@
import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/core";
interface DockerContainer { id: string; name: string; image: string; status: string; ports: string; }
interface DockerImage { repository: string; tag: string; id: string; size: string; }
interface DockerVolume { name: string; driver: string; mountpoint: string; }
const props = defineProps<{ sessionId: string }>();
const tab = ref("containers");
const containers = ref<any[]>([]);
const images = ref<any[]>([]);
const volumes = ref<any[]>([]);
const containers = ref<DockerContainer[]>([]);
const images = ref<DockerImage[]>([]);
const volumes = ref<DockerVolume[]>([]);
const output = ref("");
async function refresh(): Promise<void> {

View File

@ -1,4 +1,5 @@
import { ref, onBeforeUnmount } from "vue";
import type { Ref } from "vue";
import { invoke } from "@tauri-apps/api/core";
/**
@ -152,11 +153,11 @@ export function jsKeyToScancode(code: string): number | null {
export interface UseRdpReturn {
/** Whether the RDP session is connected (first frame received) */
connected: ReturnType<typeof ref<boolean>>;
connected: Ref<boolean>;
/** Whether keyboard capture is enabled */
keyboardGrabbed: ReturnType<typeof ref<boolean>>;
keyboardGrabbed: Ref<boolean>;
/** Whether clipboard sync is enabled */
clipboardSync: ReturnType<typeof ref<boolean>>;
clipboardSync: Ref<boolean>;
/** Fetch the current frame as RGBA ImageData */
fetchFrame: (sessionId: string, width: number, height: number) => Promise<ImageData | null>;
/** Send a mouse event to the backend */

View File

@ -24,6 +24,11 @@ export interface UseSftpReturn {
// Persist the last browsed path per session so switching tabs restores position
const sessionPaths: Record<string, string> = {};
/** Remove a session's saved path from the module-level cache. Call on session close. */
export function cleanupSession(sessionId: string): void {
delete sessionPaths[sessionId];
}
/**
* Composable that manages SFTP file browsing state.
* Accepts a reactive session ID ref so it reinitializes on tab switch

View File

@ -51,22 +51,33 @@ export const useConnectionStore = defineStore("connection", () => {
);
});
/** Memoized map of groupId → filtered connections. Recomputes only when connections or searchQuery change. */
const connectionsByGroupMap = computed<Record<number, Connection[]>>(() => {
const q = searchQuery.value.toLowerCase().trim();
const map: Record<number, Connection[]> = {};
for (const c of connections.value) {
if (c.groupId === null) continue;
if (q) {
const match =
c.name.toLowerCase().includes(q) ||
c.hostname.toLowerCase().includes(q) ||
c.tags?.some((t) => t.toLowerCase().includes(q));
if (!match) continue;
}
if (!map[c.groupId]) map[c.groupId] = [];
map[c.groupId].push(c);
}
return map;
});
/** Get connections belonging to a specific group. */
function connectionsByGroup(groupId: number): Connection[] {
const q = searchQuery.value.toLowerCase().trim();
const groupConns = connections.value.filter((c) => c.groupId === groupId);
if (!q) return groupConns;
return groupConns.filter(
(c) =>
c.name.toLowerCase().includes(q) ||
c.hostname.toLowerCase().includes(q) ||
c.tags?.some((t) => t.toLowerCase().includes(q)),
);
return connectionsByGroupMap.value[groupId] ?? [];
}
/** Check if a group has any matching connections (for search filtering). */
function groupHasResults(groupId: number): boolean {
return connectionsByGroup(groupId).length > 0;
return (connectionsByGroupMap.value[groupId]?.length ?? 0) > 0;
}
/** Load connections from the Rust backend. */
@ -101,6 +112,7 @@ export const useConnectionStore = defineStore("connection", () => {
groups,
searchQuery,
filteredConnections,
connectionsByGroupMap,
connectionsByGroup,
groupHasResults,
loadConnections,