Skip to content

Commit 2db9ac6

Browse files
committed
refactor: optimistic column toggle
1 parent 363aee6 commit 2db9ac6

File tree

2 files changed

+252
-169
lines changed

2 files changed

+252
-169
lines changed

web/src/components/Docker/DockerContainersTable.vue

Lines changed: 12 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ import { useDockerViewPreferences } from '@/composables/useDockerColumnVisibilit
2121
import { useDockerEditNavigation } from '@/composables/useDockerEditNavigation';
2222
import { useFolderOperations } from '@/composables/useFolderOperations';
2323
import { useFolderTree } from '@/composables/useFolderTree';
24+
import { usePersistentColumnVisibility } from '@/composables/usePersistentColumnVisibility';
2425
import { useTreeData } from '@/composables/useTreeData';
2526
2627
import type { DockerContainer, FlatOrganizerEntry } from '@/composables/gql/graphql';
2728
import type { DropEvent } from '@/composables/useDragDrop';
29+
import type { ColumnVisibilityTableInstance } from '@/composables/usePersistentColumnVisibility';
2830
import type { TreeRow } from '@/composables/useTreeData';
2931
import type { TableColumn } from '@nuxt/ui';
3032
import type { Component } from 'vue';
@@ -196,27 +198,7 @@ const containersRef = computed(() => props.containers);
196198
197199
const rootFolderId = computed<string>(() => props.rootFolderId || 'root');
198200
199-
interface BaseTableInstance {
200-
columnVisibility?: { value: Record<string, boolean> };
201-
tableApi?: {
202-
getAllColumns: () => Array<{
203-
id: string;
204-
getCanHide: () => boolean;
205-
getIsVisible: () => boolean;
206-
toggleVisibility: (visible: boolean) => void;
207-
}>;
208-
getColumn: (id: string) =>
209-
| {
210-
toggleVisibility: (visible: boolean) => void;
211-
}
212-
| undefined;
213-
setColumnVisibility?: (
214-
updater: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)
215-
) => void;
216-
};
217-
}
218-
219-
const baseTableRef = ref<BaseTableInstance | null>(null);
201+
const baseTableRef = ref<ColumnVisibilityTableInstance | null>(null);
220202
221203
const searchableKeys = [
222204
'name',
@@ -505,125 +487,14 @@ const resolvedColumnVisibility = computed<Record<string, boolean>>(() => ({
505487
...(columnVisibilityRef.value ?? {}),
506488
}));
507489
508-
function getEffectiveVisibility(
509-
visibility: Record<string, boolean> | undefined | null,
510-
columnId: string
511-
): boolean {
512-
if (!visibility) return true;
513-
if (Object.prototype.hasOwnProperty.call(visibility, columnId)) {
514-
return visibility[columnId];
515-
}
516-
return true;
517-
}
518-
519-
function visibilityStatesMatch(
520-
current: Record<string, boolean> | null | undefined,
521-
target: Record<string, boolean> | null | undefined,
522-
columnIds: string[] | undefined
523-
): boolean {
524-
if (!current || !target) return false;
525-
const keys =
526-
columnIds && columnIds.length > 0
527-
? new Set(columnIds)
528-
: new Set([...Object.keys(current), ...Object.keys(target)]);
529-
for (const key of keys) {
530-
if (getEffectiveVisibility(current, key) !== getEffectiveVisibility(target, key)) {
531-
return false;
532-
}
533-
}
534-
return true;
535-
}
536-
537-
function applyColumnVisibility(target: Record<string, boolean>) {
538-
const tableInstance = baseTableRef.value;
539-
if (!tableInstance?.columnVisibility) return;
540-
541-
const visibilityRef = tableInstance.columnVisibility;
542-
const tableApi = tableInstance.tableApi;
543-
const current = visibilityRef.value || {};
544-
const columnIds = tableApi
545-
? tableApi
546-
.getAllColumns()
547-
.filter((column) => column.getCanHide())
548-
.map((column) => column.id)
549-
: [];
550-
551-
if (visibilityStatesMatch(current, target, columnIds)) {
552-
return;
553-
}
554-
555-
if (tableApi?.setColumnVisibility) {
556-
tableApi.setColumnVisibility(() => ({ ...target }));
557-
} else {
558-
visibilityRef.value = { ...target };
559-
}
560-
}
561-
562-
watch(
563-
() => resolvedColumnVisibility.value,
564-
(target) => {
565-
applyColumnVisibility(target);
566-
},
567-
{ immediate: true, deep: true }
568-
);
569-
570-
watch(
571-
baseTableRef,
572-
() => {
573-
applyColumnVisibility(resolvedColumnVisibility.value);
574-
},
575-
{ immediate: true, flush: 'post' }
576-
);
577-
578-
const lastSavedColumnVisibility = ref<Record<string, boolean> | null>(null);
579-
580-
function getHideableColumnIds(): string[] {
581-
const tableApi = baseTableRef.value?.tableApi;
582-
if (tableApi) {
583-
return tableApi
584-
.getAllColumns()
585-
.filter((column) => column.getCanHide())
586-
.map((column) => column.id);
587-
}
588-
return Object.keys(defaultColumnVisibility.value);
589-
}
590-
591-
function normalizeColumnVisibilityState(
592-
raw: Record<string, boolean> | null | undefined
593-
): Record<string, boolean> {
594-
const ids = getHideableColumnIds();
595-
const normalized: Record<string, boolean> = {};
596-
for (const id of ids) {
597-
normalized[id] = getEffectiveVisibility(raw, id);
598-
}
599-
return normalized;
600-
}
601-
602-
function readCurrentColumnVisibility(): Record<string, boolean> | null {
603-
const tableApi = baseTableRef.value?.tableApi;
604-
if (!tableApi) return null;
605-
const record: Record<string, boolean> = {};
606-
for (const column of tableApi.getAllColumns()) {
607-
if (!column.getCanHide()) continue;
608-
record[column.id] = column.getIsVisible();
609-
}
610-
return record;
611-
}
612-
613-
async function persistCurrentColumnVisibility() {
614-
await nextTick();
615-
const current = readCurrentColumnVisibility();
616-
if (!current) return;
617-
const normalized = normalizeColumnVisibilityState(current);
618-
if (
619-
lastSavedColumnVisibility.value &&
620-
visibilityStatesMatch(normalized, lastSavedColumnVisibility.value, Object.keys(normalized))
621-
) {
622-
return;
623-
}
624-
lastSavedColumnVisibility.value = { ...normalized };
625-
saveColumnVisibility({ ...normalized });
626-
}
490+
// Keep table visibility in sync with saved preferences and persist optimistic user toggles.
491+
const { persistCurrentColumnVisibility } = usePersistentColumnVisibility({
492+
tableRef: baseTableRef,
493+
resolvedVisibility: resolvedColumnVisibility,
494+
fallbackVisibility: defaultColumnVisibility,
495+
onPersist: (visibility) => saveColumnVisibility({ ...visibility }),
496+
isPersistenceEnabled: () => !props.compact,
497+
});
627498
628499
watch(
629500
() => props.viewPrefs,
@@ -635,34 +506,6 @@ watch(
635506
{ immediate: true }
636507
);
637508
638-
watch(
639-
() => baseTableRef.value?.columnVisibility?.value,
640-
(columnVisibility) => {
641-
if (!columnVisibility || props.compact) {
642-
return;
643-
}
644-
645-
const columnIds = getHideableColumnIds();
646-
const normalizedCurrent = normalizeColumnVisibilityState(columnVisibility);
647-
648-
if (visibilityStatesMatch(normalizedCurrent, resolvedColumnVisibility.value, columnIds)) {
649-
lastSavedColumnVisibility.value = { ...normalizedCurrent };
650-
return;
651-
}
652-
653-
if (
654-
lastSavedColumnVisibility.value &&
655-
visibilityStatesMatch(normalizedCurrent, lastSavedColumnVisibility.value, columnIds)
656-
) {
657-
return;
658-
}
659-
660-
lastSavedColumnVisibility.value = { ...normalizedCurrent };
661-
saveColumnVisibility({ ...normalizedCurrent });
662-
},
663-
{ deep: true }
664-
);
665-
666509
type ActionDropdownItem = { label: string; icon?: string; onSelect?: (e?: Event) => void; as?: string };
667510
type DropdownMenuItems = ActionDropdownItem[][];
668511
@@ -699,7 +542,7 @@ const columnsMenuItems = computed<DropdownMenuItems>(() => {
699542
type: 'checkbox' as const,
700543
checked: column.getIsVisible(),
701544
onUpdateChecked(checked: boolean) {
702-
baseTableRef.value?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked);
545+
baseTableRef.value?.tableApi?.getColumn?.(column.id)?.toggleVisibility(!!checked);
703546
void persistCurrentColumnVisibility();
704547
},
705548
onSelect(e: Event) {

0 commit comments

Comments
 (0)