Fix inline editor persistence during websocket updates

Critical fixes to prevent the inline URL editor from closing during API updates:

1. Implement stable guest store with reconcile:
   - Use createStore with reconcile() to maintain stable object references
   - Key function ensures each guest keeps same proxy instance across updates
   - Prevents <For> loop from remounting rows during websocket updates

2. Allow switching between guest editors:
   - Mark guest name spans with data-guest-name-editable attribute
   - Click handler allows clicking another guest name to switch editors
   - Prevents click consumption when opening a different guest's editor

This ensures the inline editor stays open and preserves user input even when
websocket updates arrive, while still allowing seamless switching between
editing different guests.
This commit is contained in:
rcourtman 2025-10-23 17:45:54 +00:00
parent 15252c95ce
commit 32cd925e1f
2 changed files with 22 additions and 9 deletions

View file

@ -1,4 +1,5 @@
import { createSignal, createMemo, createEffect, For, Show, onMount } from 'solid-js';
import { createStore, reconcile } from 'solid-js/store';
import { useNavigate } from '@solidjs/router';
import type { VM, Container, Node } from '@/types/api';
import { GuestRow } from './GuestRow';
@ -41,6 +42,16 @@ export function Dashboard(props: DashboardProps) {
const [selectedNode, setSelectedNode] = createSignal<string | null>(null);
const [guestMetadata, setGuestMetadata] = createSignal<Record<string, GuestMetadata>>({});
// Stable guest store using reconcile to prevent row remounting during websocket updates
const guestKey = (g: VM | Container) =>
g.id ?? (g.instance === g.node ? `${g.node}-${g.vmid}` : `${g.instance}-${g.node}-${g.vmid}`);
const [guestStore, setGuestStore] = createStore<(VM | Container)[]>([]);
// Reconcile guests whenever props change
createEffect(() => {
setGuestStore(reconcile([...props.vms, ...props.containers], { key: guestKey, merge: true }));
});
// Initialize from localStorage with proper type checking
const [viewMode, setViewMode] = usePersistentSignal<ViewMode>('dashboardViewMode', 'all', {
deserialize: (raw) => (raw === 'all' || raw === 'vm' || raw === 'lxc' ? raw : 'all'),
@ -237,12 +248,8 @@ export function Dashboard(props: DashboardProps) {
return () => document.removeEventListener('keydown', handleKeyDown);
});
// Combine VMs and containers into a single list
const allGuests = createMemo(() => {
const vms = props.vms || [];
const containers = props.containers || [];
return [...vms, ...containers];
});
// Use the stable guest store
const allGuests = createMemo(() => guestStore);
// Filter guests based on current settings
const filteredGuests = createMemo(() => {

View file

@ -192,8 +192,11 @@ export function GuestRow(props: GuestRowProps) {
if (isEditingUrl()) {
const handleGlobalClick = (e: MouseEvent) => {
const target = e.target as HTMLElement;
// If clicking outside the editor, close it and prevent the click
if (!target.closest('[data-url-editor]') && currentlyEditingGuestId() === guestId()) {
// Allow clicking another guest name to switch editing
const isClickingGuestName = target.closest('[data-guest-name-editable]');
// If clicking outside the editor (and not another guest name), close it and prevent the click
if (!target.closest('[data-url-editor]') && !isClickingGuestName && currentlyEditingGuestId() === guestId()) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
@ -203,7 +206,9 @@ export function GuestRow(props: GuestRowProps) {
const handleGlobalMouseDown = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('[data-url-editor]') && currentlyEditingGuestId() === guestId()) {
const isClickingGuestName = target.closest('[data-guest-name-editable]');
if (!target.closest('[data-url-editor]') && !isClickingGuestName && currentlyEditingGuestId() === guestId()) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
@ -407,6 +412,7 @@ export function GuestRow(props: GuestRowProps) {
style="cursor: text;"
title={`${props.guest.name}${customUrl() ? ' - Click to edit URL' : ' - Click to add URL'}`}
onClick={startEditingUrl}
data-guest-name-editable
>
{props.guest.name}
</span>