column visibility fixed in this
This commit is contained in:
@@ -1,171 +1,53 @@
|
||||
/**
|
||||
* Column Visibility Store (Zustand)
|
||||
* Manages column visibility settings per page with localStorage persistence
|
||||
* Uses the same pattern as siteStore and sectorStore
|
||||
* Stores preferences per user for multi-user support
|
||||
* Column preferences expire after 30 days
|
||||
* Simple localStorage-based column visibility per page per user
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
|
||||
|
||||
interface ColumnPreference {
|
||||
columns: string[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface ColumnVisibilityState {
|
||||
// Map of page pathname to column preference with timestamp
|
||||
pageColumns: Record<string, ColumnPreference>;
|
||||
|
||||
// Hydration flag to know when persist has loaded
|
||||
_hasHydrated: boolean;
|
||||
setHasHydrated: (state: boolean) => void;
|
||||
|
||||
// Actions
|
||||
setPageColumns: (pathname: string, columnKeys: string[]) => void;
|
||||
getPageColumns: (pathname: string) => string[];
|
||||
toggleColumn: (pathname: string, columnKey: string) => void;
|
||||
resetPageColumns: (pathname: string) => void;
|
||||
getVisibleColumns: (pathname: string) => string[];
|
||||
setVisibleColumns: (pathname: string, columns: string[]) => void;
|
||||
}
|
||||
|
||||
// Helper function to get user ID directly from localStorage (synchronous, no race conditions)
|
||||
// CRITICAL: We must read directly from localStorage, not from useAuthStore.getState().user
|
||||
// because the auth store might not be hydrated yet when this storage is accessed
|
||||
const getUserIdFromStorage = (): string => {
|
||||
// Get user ID from auth storage
|
||||
const getUserId = (): string => {
|
||||
try {
|
||||
const authData = localStorage.getItem('auth-storage');
|
||||
if (authData) {
|
||||
const parsed = JSON.parse(authData);
|
||||
const userId = parsed?.state?.user?.id;
|
||||
if (userId) {
|
||||
return String(userId);
|
||||
return String(parsed?.state?.user?.id || 'anonymous');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silent fail - will use anonymous
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return 'anonymous';
|
||||
};
|
||||
|
||||
// Custom storage that uses user-specific keys
|
||||
// IMPORTANT: The persist middleware passes the 'name' param, but we need to append user ID
|
||||
// CRITICAL FIX: Get user ID directly from localStorage synchronously to avoid race conditions
|
||||
const userSpecificStorage: StateStorage = {
|
||||
getItem: (name: string) => {
|
||||
const userId = getUserIdFromStorage();
|
||||
const key = `${name}-user-${userId}`;
|
||||
const value = localStorage.getItem(key);
|
||||
if (typeof window !== 'undefined' && window.location.pathname.includes('/writer/')) {
|
||||
console.log('🔍 STORAGE GET:', { key, hasValue: !!value, valuePreview: value?.substring(0, 50) });
|
||||
}
|
||||
return value;
|
||||
},
|
||||
setItem: (name: string, value: string) => {
|
||||
const userId = getUserIdFromStorage();
|
||||
const key = `${name}-user-${userId}`;
|
||||
if (typeof window !== 'undefined' && window.location.pathname.includes('/writer/')) {
|
||||
console.log('💾 STORAGE SET:', { key, valuePreview: value?.substring(0, 50) });
|
||||
}
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
removeItem: (name: string) => {
|
||||
const userId = getUserIdFromStorage();
|
||||
const key = `${name}-user-${userId}`;
|
||||
localStorage.removeItem(key);
|
||||
},
|
||||
// Build storage key
|
||||
const getStorageKey = (pathname: string): string => {
|
||||
return `columns-${getUserId()}-${pathname}`;
|
||||
};
|
||||
|
||||
// 30 days in milliseconds
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export const useColumnVisibilityStore = create<ColumnVisibilityState>()(
|
||||
persist<ColumnVisibilityState>(
|
||||
(set, get) => ({
|
||||
pageColumns: {},
|
||||
_hasHydrated: false,
|
||||
|
||||
setHasHydrated: (state: boolean) => {
|
||||
set({ _hasHydrated: state });
|
||||
},
|
||||
|
||||
setPageColumns: (pathname: string, columnKeys: string[]) => {
|
||||
if (pathname.includes('/writer/')) {
|
||||
console.log('📝 setPageColumns:', { pathname, columns: columnKeys, timestamp: new Date().toISOString() });
|
||||
export const useColumnVisibilityStore = create<ColumnVisibilityState>((set, get) => ({
|
||||
getVisibleColumns: (pathname: string) => {
|
||||
try {
|
||||
const key = getStorageKey(pathname);
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
set((state) => ({
|
||||
pageColumns: {
|
||||
...state.pageColumns,
|
||||
[pathname]: {
|
||||
columns: columnKeys,
|
||||
timestamp: Date.now(),
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return [];
|
||||
},
|
||||
|
||||
setVisibleColumns: (pathname: string, columns: string[]) => {
|
||||
try {
|
||||
const key = getStorageKey(pathname);
|
||||
localStorage.setItem(key, JSON.stringify(columns));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
getPageColumns: (pathname: string) => {
|
||||
const preference = get().pageColumns[pathname];
|
||||
if (pathname.includes('/writer/')) {
|
||||
console.log('📖 getPageColumns:', { pathname, hasPreference: !!preference, columns: preference?.columns });
|
||||
}
|
||||
if (!preference) return [];
|
||||
|
||||
// Check if preference has expired (older than 30 days)
|
||||
const now = Date.now();
|
||||
if (now - preference.timestamp > THIRTY_DAYS_MS) {
|
||||
// Remove expired preference
|
||||
set((state) => {
|
||||
const newPageColumns = { ...state.pageColumns };
|
||||
delete newPageColumns[pathname];
|
||||
return { pageColumns: newPageColumns };
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
return preference.columns;
|
||||
},
|
||||
|
||||
toggleColumn: (pathname: string, columnKey: string) => {
|
||||
set((state) => {
|
||||
const preference = state.pageColumns[pathname];
|
||||
const currentColumns = preference?.columns || [];
|
||||
const newColumns = currentColumns.includes(columnKey)
|
||||
? currentColumns.filter((key) => key !== columnKey)
|
||||
: [...currentColumns, columnKey];
|
||||
|
||||
return {
|
||||
pageColumns: {
|
||||
...state.pageColumns,
|
||||
[pathname]: {
|
||||
columns: newColumns,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
resetPageColumns: (pathname: string) => {
|
||||
set((state) => {
|
||||
const newPageColumns = { ...state.pageColumns };
|
||||
delete newPageColumns[pathname];
|
||||
return { pageColumns: newPageColumns };
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'igny8-column-visibility',
|
||||
storage: createJSONStorage(() => userSpecificStorage),
|
||||
partialize: (state) => ({
|
||||
pageColumns: state.pageColumns,
|
||||
}),
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (state && typeof window !== 'undefined' && window.location.pathname.includes('/writer/')) {
|
||||
console.log('💧 REHYDRATED:', { pageColumns: state.pageColumns });
|
||||
}
|
||||
state?.setHasHydrated(true);
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -240,71 +240,22 @@ export default function TablePageTemplate({
|
||||
const { setMetrics } = useHeaderMetrics();
|
||||
const toast = useToast();
|
||||
const { pageSize, setPageSize } = usePageSizeStore();
|
||||
const { setPageColumns, getPageColumns, _hasHydrated } = useColumnVisibilityStore();
|
||||
const { getVisibleColumns, setVisibleColumns: saveVisibleColumns } = useColumnVisibilityStore();
|
||||
|
||||
// Column visibility state management with Zustand store
|
||||
// Start with defaults, then update once hydration completes
|
||||
// Column visibility - load from localStorage on mount, save on change
|
||||
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(() => {
|
||||
// Always start with defaults on initial render (before hydration)
|
||||
return new Set(
|
||||
columns
|
||||
.filter(col => col.defaultVisible !== false)
|
||||
.map(col => col.key)
|
||||
);
|
||||
const saved = getVisibleColumns(location.pathname);
|
||||
if (saved.length > 0) {
|
||||
return new Set(saved);
|
||||
}
|
||||
// Default: all columns with defaultVisible !== false
|
||||
return new Set(columns.filter(col => col.defaultVisible !== false).map(col => col.key));
|
||||
});
|
||||
|
||||
// Track if we've already loaded saved columns for this page to prevent loops
|
||||
const hasLoadedSavedColumns = useRef(false);
|
||||
|
||||
// Load saved columns from store after hydration completes (one-time per page)
|
||||
// Save to localStorage whenever columns change
|
||||
useEffect(() => {
|
||||
console.log('🔄 LOAD EFFECT:', { hasHydrated: _hasHydrated, hasLoaded: hasLoadedSavedColumns.current, pathname: location.pathname });
|
||||
if (!_hasHydrated || hasLoadedSavedColumns.current) return;
|
||||
|
||||
// Mark as loaded FIRST to prevent save effect from firing during this update
|
||||
hasLoadedSavedColumns.current = true;
|
||||
console.log('✅ Marked hasLoaded = true');
|
||||
|
||||
const savedColumnKeys = getPageColumns(location.pathname);
|
||||
console.log('📥 LOADING saved columns:', savedColumnKeys);
|
||||
|
||||
if (savedColumnKeys && savedColumnKeys.length > 0) {
|
||||
const savedSet = new Set(savedColumnKeys);
|
||||
// Validate that all saved columns still exist in current columns config
|
||||
const validColumns = columns.filter(col => savedSet.has(col.key));
|
||||
|
||||
if (validColumns.length > 0) {
|
||||
// Add any new columns with defaultVisible !== false
|
||||
const newColumns = columns
|
||||
.filter(col => !savedSet.has(col.key) && col.defaultVisible !== false)
|
||||
.map(col => col.key);
|
||||
const mergedColumns = [...Array.from(validColumns.map(col => col.key)), ...newColumns];
|
||||
console.log('✅ Setting visible columns to:', mergedColumns);
|
||||
setVisibleColumns(new Set(mergedColumns));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('⚠️ No valid saved columns, keeping defaults');
|
||||
}, [_hasHydrated, location.pathname]);
|
||||
// NOTE: NOT including 'getPageColumns' or 'columns' - would cause unnecessary re-runs
|
||||
|
||||
// Reset loaded flag when pathname changes (navigating to different page)
|
||||
useEffect(() => {
|
||||
hasLoadedSavedColumns.current = false;
|
||||
}, [location.pathname]);
|
||||
|
||||
// Save to store whenever visibleColumns changes (only after hydration to avoid saving defaults)
|
||||
useEffect(() => {
|
||||
console.log('💾 SAVE EFFECT:', {
|
||||
hasHydrated: _hasHydrated,
|
||||
hasLoaded: hasLoadedSavedColumns.current,
|
||||
willSave: _hasHydrated && hasLoadedSavedColumns.current,
|
||||
columns: Array.from(visibleColumns)
|
||||
});
|
||||
if (!_hasHydrated || !hasLoadedSavedColumns.current) return; // Don't save until hydration completes AND saved columns have been loaded
|
||||
setPageColumns(location.pathname, Array.from(visibleColumns));
|
||||
}, [_hasHydrated, visibleColumns, location.pathname, setPageColumns]);
|
||||
saveVisibleColumns(location.pathname, Array.from(visibleColumns));
|
||||
}, [visibleColumns, location.pathname, saveVisibleColumns]);
|
||||
|
||||
// Filter columns based on visibility
|
||||
const visibleColumnsList = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user