refactor: Vue 3 state, TypeScript, and lifecycle improvements
All checks were successful
Build & Sign Wraith / Build Windows + Sign (push) Successful in 3m49s
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:
parent
ebd3cee49e
commit
28619bba3f
@ -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[] = [
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user