Skip to content

Commit d67e8c3

Browse files
committed
implement column width adjustment with local storage support
1 parent 9023ec9 commit d67e8c3

3 files changed

Lines changed: 151 additions & 16 deletions

File tree

src/plugins/GridViewPlugin/components/Table/Table.tsx

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,16 @@ export const Table: React.FC<TableProps> = ({
254254
handleUpdateFilter: handleUpdateFilterFromHook,
255255
handleGroupByChange,
256256
handleSortChange,
257+
handleColumnWidthChange,
257258
handleEnsureAllFieldsRegistered,
258259
handleFieldToggle,
259260
handleFieldOrderChange,
260261
updateViewConfigBackend,
262+
localColumnWidths,
261263
} = useTableViewConfig({
262264
baseMeta,
263265
effectiveViewId,
266+
tableId,
264267
columns,
265268
updateViewMutation: actions?.updateView,
266269
searchableColumns,
@@ -376,25 +379,33 @@ export const Table: React.FC<TableProps> = ({
376379
const [openColumnDropdownIndex, setOpenColumnDropdownIndex] = useState<number | null>(null);
377380
const dragPinnedStateRef = useRef<boolean | null>(null);
378381

379-
// Static column widths (no resize). Prefer view meta widths, else column.width, else 235.
382+
// Column resize state
383+
const [resizingColumnIndex, setResizingColumnIndex] = useState<number | null>(null);
384+
const [liveColumnWidths, setLiveColumnWidths] = useState<number[] | null>(null);
385+
const resizeStartRef = useRef<{ x: number; width: number } | null>(null);
386+
const columnWidthsRef = useRef<number[]>([]);
387+
388+
// Column widths: live drag preview > localStorage > view meta > column.width > 235.
380389
const columnWidths = useMemo(() => {
381-
return (visibleColumns || []).map((c) => {
390+
return (visibleColumns || []).map((c, idx) => {
382391
const widthKey = String(c.id || c.key);
392+
// 1. Live drag preview (highest priority during resize)
393+
if (liveColumnWidths && liveColumnWidths[idx] != null) return liveColumnWidths[idx];
394+
// 2. localStorage (user's saved overrides)
395+
const fromLocal = (localColumnWidths ?? {})[widthKey] ?? (localColumnWidths ?? {})[c.key];
396+
if (typeof fromLocal === 'number') return fromLocal;
397+
// 3. view meta (backend)
383398
const fromView = viewConfigState.columnWidths?.[widthKey] ?? viewConfigState.columnWidths?.[c.key];
384-
385-
let width = 235;
386-
387-
if (typeof c.width === 'number') {
388-
width = c.width;
389-
}
390-
391-
if (typeof fromView === 'number') {
392-
width = fromView;
393-
}
394-
395-
return width;
399+
if (typeof fromView === 'number') return fromView;
400+
// 4. column.width (API)
401+
if (typeof c.width === 'number') return c.width;
402+
// 5. default
403+
return 235;
396404
});
397-
}, [visibleColumns, viewConfigState.columnWidths]);
405+
}, [visibleColumns, viewConfigState.columnWidths, localColumnWidths, liveColumnWidths]);
406+
407+
// Keep columnWidthsRef in sync with the computed columnWidths
408+
useEffect(() => { columnWidthsRef.current = columnWidths; }, [columnWidths]);
398409

399410
// Keep only first visible column pinned by default (Noco-like behavior)
400411
const pinnedColumnIds = useMemo(() => {
@@ -724,6 +735,48 @@ export const Table: React.FC<TableProps> = ({
724735
}
725736
}, [canReorderColumns, handleColumnDragEndFromHook, visibleColumns, localFieldConfig, effectiveViewId, baseMeta, actions?.updateView, handleFieldOrderChange]);
726737

738+
// Column resize: start drag
739+
const handleResizeMouseDown = useCallback((e: React.MouseEvent, columnIndex: number) => {
740+
e.preventDefault();
741+
e.stopPropagation();
742+
const currentWidths = columnWidthsRef.current;
743+
setResizingColumnIndex(columnIndex);
744+
resizeStartRef.current = {
745+
x: e.clientX,
746+
width: currentWidths[columnIndex] ?? 235,
747+
};
748+
749+
const handleMouseMove = (moveEvent: MouseEvent) => {
750+
if (!resizeStartRef.current) return;
751+
const delta = moveEvent.clientX - resizeStartRef.current.x;
752+
const newWidth = Math.max(80, resizeStartRef.current.width + delta);
753+
setLiveColumnWidths(prev => {
754+
const base = prev ?? currentWidths;
755+
const next = [...base];
756+
next[columnIndex] = newWidth;
757+
return next;
758+
});
759+
};
760+
761+
const handleMouseUp = (upEvent: MouseEvent) => {
762+
if (!resizeStartRef.current) return;
763+
const delta = upEvent.clientX - resizeStartRef.current.x;
764+
const newWidth = Math.max(80, resizeStartRef.current.width + delta);
765+
const column = visibleColumns[columnIndex];
766+
if (column) {
767+
handleColumnWidthChange(String(column.id || column.key), newWidth);
768+
}
769+
resizeStartRef.current = null;
770+
setResizingColumnIndex(null);
771+
setLiveColumnWidths(null);
772+
document.removeEventListener('mousemove', handleMouseMove);
773+
document.removeEventListener('mouseup', handleMouseUp);
774+
};
775+
776+
document.addEventListener('mousemove', handleMouseMove);
777+
document.addEventListener('mouseup', handleMouseUp);
778+
}, [visibleColumns, handleColumnWidthChange]);
779+
727780
// Wrapper for handleEditColumn to use hook's version
728781
const handleEditColumn = useCallback((col: ColumnConfig, index: number, event?: { target: HTMLElement }) => {
729782
handleEditColumnFromHook(col, index, event);
@@ -857,6 +910,14 @@ export const Table: React.FC<TableProps> = ({
857910
onOpenChange={handleDropdownOpenChange}
858911
/>
859912
)}
913+
{/* Column resize handle */}
914+
<div
915+
className={`absolute right-0 top-0 h-full w-1 cursor-col-resize z-10 transition-colors
916+
${resizingColumnIndex === index ? 'bg-primary' : 'hover:bg-primary/60 bg-transparent'}
917+
`}
918+
style={{ top: 0 }}
919+
onMouseDown={(e) => handleResizeMouseDown(e, index)}
920+
/>
860921
</div>
861922
</div>
862923
);
@@ -878,7 +939,9 @@ export const Table: React.FC<TableProps> = ({
878939
openColumnDropdownIndex,
879940
canShowColumnDropdown,
880941
getColumnHeaderClassName,
881-
setOpenColumnDropdownIndex
942+
setOpenColumnDropdownIndex,
943+
handleResizeMouseDown,
944+
resizingColumnIndex,
882945
]);
883946

884947
const getEstimatedItemCount = useCallback(() => {

src/plugins/GridViewPlugin/hooks/useTableViewConfig.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { parseFieldConfig } from '../../../utils/pluginUtils';
88
import { filterValidSorts } from '../../../utils/sortUtils';
99
import { GridColumn as ColumnConfig } from '../types/grid.types';
1010
import { SearchField } from '../../../hooks/useSearch';
11+
import { getColumnWidths, saveColumnWidths } from '../utils/columnWidthStorage';
1112

1213
export type FilterType = { column: string; operator: string; value: string };
1314
export type GroupByItem = {
@@ -27,6 +28,7 @@ export interface ViewConfigState {
2728
interface UseTableViewConfigOptions {
2829
baseMeta?: Record<string, any>;
2930
effectiveViewId?: string;
31+
tableId?: string;
3032
columns: ColumnConfig[];
3133
updateViewMutation?: any;
3234
searchableColumns: ColumnConfig[];
@@ -36,6 +38,7 @@ interface UseTableViewConfigOptions {
3638
export function useTableViewConfig({
3739
baseMeta,
3840
effectiveViewId,
41+
tableId,
3942
columns,
4043
updateViewMutation,
4144
searchableColumns,
@@ -64,6 +67,17 @@ export function useTableViewConfig({
6467
// Local field configuration state
6568
const [localFieldConfig, setLocalFieldConfig] = useState<any[]>([]);
6669

70+
// Local column widths from localStorage (user overrides)
71+
const [localColumnWidths, setLocalColumnWidths] = useState<Record<string, number>>({});
72+
73+
// Load column widths from localStorage when table/view changes
74+
useEffect(() => {
75+
if (tableId && effectiveViewId) {
76+
const stored = getColumnWidths(tableId, effectiveViewId);
77+
setLocalColumnWidths(stored);
78+
}
79+
}, [tableId, effectiveViewId]);
80+
6781
// Helper to parse/normalize viewConfig/meta
6882
const getConfigObj = useCallback((config: any) => {
6983
return parseFieldConfig(config);
@@ -257,6 +271,18 @@ export function useTableViewConfig({
257271
}
258272
}, [viewConfigState, updateViewConfigBackend, isReadOnly]);
259273

274+
// Update a single column width and persist to localStorage only (not backend)
275+
const handleColumnWidthChange = useCallback((columnId: string, width: number) => {
276+
const clampedWidth = Math.max(80, width); // enforce minimum
277+
setLocalColumnWidths(prev => {
278+
const next = { ...prev, [columnId]: clampedWidth };
279+
if (tableId && effectiveViewId) {
280+
saveColumnWidths(tableId, effectiveViewId, next);
281+
}
282+
return next;
283+
});
284+
}, [tableId, effectiveViewId]);
285+
260286
// Ensure all fields are registered in fieldConfig when FieldsPopover opens
261287
const handleEnsureAllFieldsRegistered = useCallback(async () => {
262288
const existing = Array.isArray(baseMeta?.fieldConfig) ? baseMeta.fieldConfig : [];
@@ -392,9 +418,11 @@ export function useTableViewConfig({
392418
handleUpdateFilter,
393419
handleGroupByChange,
394420
handleSortChange,
421+
handleColumnWidthChange,
395422
handleEnsureAllFieldsRegistered,
396423
handleFieldToggle,
397424
handleFieldOrderChange,
398425
updateViewConfigBackend,
426+
localColumnWidths,
399427
};
400428
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2026 Aptlogica Technologies Private Limited
2+
// SPDX-License-Identifier: MIT
3+
// Websites: https://www.aptlogica.com | https://www.serenibase.com
4+
// Support: support@aptlogica.com | support@serenibase.com
5+
6+
const STORAGE_KEY_PREFIX = 'serenibase_col_widths';
7+
8+
const getStorageKey = (tableId: string, viewId: string): string => {
9+
return `${STORAGE_KEY_PREFIX}_${tableId}_${viewId}`;
10+
};
11+
12+
export const getColumnWidths = (tableId: string, viewId: string): Record<string, number> => {
13+
try {
14+
const key = getStorageKey(tableId, viewId);
15+
const stored = localStorage.getItem(key);
16+
if (stored) {
17+
const parsed = JSON.parse(stored);
18+
if (typeof parsed === 'object' && parsed !== null) {
19+
return parsed as Record<string, number>;
20+
}
21+
}
22+
} catch (error) {
23+
console.error('[columnWidthStorage] Failed to read column widths:', error);
24+
}
25+
return {};
26+
};
27+
28+
export const saveColumnWidths = (tableId: string, viewId: string, widths: Record<string, number>): void => {
29+
try {
30+
const key = getStorageKey(tableId, viewId);
31+
localStorage.setItem(key, JSON.stringify(widths));
32+
} catch (error) {
33+
console.error('[columnWidthStorage] Failed to save column widths:', error);
34+
}
35+
};
36+
37+
export const clearColumnWidths = (tableId: string, viewId: string): void => {
38+
try {
39+
const key = getStorageKey(tableId, viewId);
40+
localStorage.removeItem(key);
41+
} catch (error) {
42+
console.error('[columnWidthStorage] Failed to clear column widths:', error);
43+
}
44+
};

0 commit comments

Comments
 (0)