Skip to content

Commit

Permalink
Merge pull request #17783 from GordonSmith/HPCC-30303_PERSIST_COLUMN
Browse files Browse the repository at this point in the history
HPCC-30303 Persist columns widths per page

Reviewed-By: Jeremy Clements <[email protected]>
Merged-by: Gavin Halliday <[email protected]>
  • Loading branch information
ghalliday authored Sep 19, 2023
2 parents bfa290d + cb1ed4c commit 6b4398a
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 35 deletions.
1 change: 1 addition & 0 deletions esp/src/src-react/components/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const Files: React.FunctionComponent<FilesProps> = ({
},
Name: {
label: nlsHPCC.LogicalName,
width: 360,
formatter: (name, row) => {
const file = Get(row.NodeGroup, name, row);
if (row.__hpcc_isDir) {
Expand Down
2 changes: 1 addition & 1 deletion esp/src/src-react/components/Title.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const DevTitle: React.FunctionComponent<DevTitleProps> = ({
method: "post"
}).then(() => {
setUserSession({ ...userSession, Status: "Locked" });
replaceUrl("/login", null, true);
replaceUrl("/login", true);
});
}
},
Expand Down
26 changes: 19 additions & 7 deletions esp/src/src-react/components/controls/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import nlsHPCC from "src/nlsHPCC";
import { createCopyDownloadSelection } from "../Common";
import { updatePage, updateSort } from "../../util/history";
import { useDeepCallback, useDeepEffect, useDeepMemo } from "../../hooks/deepHooks";
import { useUserStore } from "../../hooks/store";
import { useUserStore, useNonReactiveEphemeralPageStore } from "../../hooks/store";
import { useUserTheme } from "../../hooks/theme";

/* --- Debugging dependency changes ---
Expand Down Expand Up @@ -65,24 +65,30 @@ function updateColumnSorted(columns: IColumn[], attr: any, desc: boolean) {
}
}

function columnsAdapter(columns: FluentColumns): IColumn[] {
function columnsAdapter(columns: FluentColumns, columnWidths: Map<string, any>): IColumn[] {
const retVal: IColumn[] = [];
for (const key in columns) {
const column = columns[key];
const width = columnWidths.get(key) ?? column.width;
if (column?.selectorType === undefined && column?.hidden !== true) {
retVal.push({
key,
name: column.label ?? key,
fieldName: column.field ?? key,
minWidth: column.width ?? 70,
maxWidth: column.width,
minWidth: width ?? 70,
maxWidth: width,
isResizable: true,
isSorted: false,
isSortedDescending: false,
iconName: column.headerIcon,
isIconOnly: !!column.headerIcon,
data: column,
onRender: tooltipItemRenderer
styles: { root: { width } },
onRender: (item: any, index: number, col: IColumn) => {
col.minWidth = column.width ?? 70;
col.maxWidth = column.width;
return tooltipItemRenderer(item, index, col);
}
} as IColumn);
}
}
Expand Down Expand Up @@ -187,6 +193,7 @@ const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
const memoizedColumns = useDeepMemo(() => columns, [], [columns]);
const [sorted, setSorted] = React.useState<QuerySortItem>(sort);
const [items, setItems] = React.useState<any[]>([]);
const [columnWidths] = useNonReactiveEphemeralPageStore("columnWidths");

const selectionHandler = useConst(new Selection({
onSelectionChanged: () => {
Expand Down Expand Up @@ -220,8 +227,8 @@ const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
}, [], [sort]);

const fluentColumns: IColumn[] = React.useMemo(() => {
return columnsAdapter(memoizedColumns);
}, [memoizedColumns]);
return columnsAdapter(memoizedColumns, columnWidths);
}, [columnWidths, memoizedColumns]);

React.useEffect(() => {
updateColumnSorted(fluentColumns, sorted?.attribute as string, sorted?.descending);
Expand Down Expand Up @@ -258,6 +265,10 @@ const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
});
}, []);

const columnResize = React.useCallback((column: IColumn, newWidth: number, columnIndex?: number) => {
columnWidths.set(column.key, newWidth);
}, [columnWidths]);

return <DetailsList
compact={true}
items={items}
Expand All @@ -269,6 +280,7 @@ const FluentStoreGrid: React.FunctionComponent<FluentStoreGridProps> = ({
selectionPreservedOnEmptyClick={true}
onColumnHeaderClick={onColumnClick}
onRenderDetailsHeader={renderDetailsHeader}
onColumnResize={columnResize}
styles={gridStyles(height)}
/>;
};
Expand Down
2 changes: 1 addition & 1 deletion esp/src/src-react/components/forms/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const Login: React.FunctionComponent<LoginProps> = ({
} else {
createUserSession(cookies).then(() => {
setErrorMessage("");
replaceUrl("/", null, true);
replaceUrl("/", true);
}).catch(err => logger.error("Unable to create user session."));
}
}
Expand Down
53 changes: 49 additions & 4 deletions esp/src/src-react/hooks/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from "react";
import { useConst } from "@fluentui/react-hooks";
import { globalKeyValStore, IKeyValStore, userKeyValStore } from "src/KeyValStore";
import { globalKeyValStore, IKeyValStore, localKeyValStore, sessionKeyValStore, userKeyValStore } from "src/KeyValStore";
import { parseHash } from "../util/history";

function toString<T>(value: T, defaultValue: T): string {
if (value === undefined) value = defaultValue;
Expand Down Expand Up @@ -79,14 +80,58 @@ function useStore<T>(store: IKeyValStore, key: string, defaultValue: T, monitor:
return [value, extSetValue, reset];
}

export function useUserStore<T>(key: string, defaultValue: T, monitor: boolean = false) {
export function useGlobalStore<T>(key: string, defaultValue: T, monitor: boolean = false) {
const store = useConst(() => globalKeyValStore());
return useStore<T>(store, key, defaultValue, monitor);
}

export function useUserStore<T>(key: string, defaultValue: T, monitor: boolean = false) {
const store = useConst(() => userKeyValStore());
return useStore<T>(store, key, defaultValue, monitor);
}

export function useGlobalStore<T>(key: string, defaultValue: T, monitor: boolean = false) {
export function useLocalStore<T>(key: string, defaultValue: T, monitor: boolean = false) {
const store = useConst(() => localKeyValStore());
return useStore<T>(store, key, defaultValue, monitor);
}

const store = useConst(() => globalKeyValStore());
export function useSessionStore<T>(key: string, defaultValue: T, monitor: boolean = false) {
const store = useConst(() => sessionKeyValStore());
return useStore<T>(store, key, defaultValue, monitor);
}

/* Ephemeral Store
This store is used to persist data that is only needed for the current page.
It is only persisted in a global variable.
It is also non reactive = i.e. changing its content will not trigger a re-render.
*/

const g_state: Map<string, Map<string, Map<string, any>>> = new Map();

function useNonReactiveEphemeralPageGlobalStore<T>(): [Map<string, Map<string, T>>, () => void] {
const pathname = useConst(() => parseHash(window.location.hash).pathname);

const reset = React.useCallback(() => {
g_state.set(pathname, new Map());
}, [pathname]);

if (!g_state.has(pathname)) {
reset();
}

return [g_state.get(pathname), reset];
}

export function useNonReactiveEphemeralPageStore<T>(id: string): [Map<string, T>, () => void] {
const [pageStates] = useNonReactiveEphemeralPageGlobalStore<T>();

const reset = React.useCallback(() => {
pageStates.set(id, new Map());
}, [id, pageStates]);

if (!pageStates.has(id)) {
reset();
}

return [pageStates.get(id), reset];
}
55 changes: 33 additions & 22 deletions esp/src/src-react/util/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function resolve(pathnameOrContext: string | ResolveContext) {
return g_router.resolve(pathnameOrContext);
}

function parseHash(hash: string): HistoryLocation {
export function parseHash(hash: string): HistoryLocation {
if (hash[0] !== "#") {
return {
pathname: "/",
Expand Down Expand Up @@ -82,6 +82,7 @@ interface HistoryLocation {
pathname: string;
search: string;
id: string;
state?: { [key: string]: any }
}

export type ListenerCallback<S extends object = object> = (location: HistoryLocation, action: string) => void;
Expand Down Expand Up @@ -140,19 +141,19 @@ class History<S extends object = object> {
return hashUrl;
}

push(to: { pathname?: string, search?: string }, state?: S) {
push(to: { pathname?: string, search?: string }) {
const newHash = this.fixHash(`${this.trimRightSlash(to.pathname || this.location.pathname)}${to.search || ""}`);
if (window.location.hash !== newHash) {
globalHistory.pushState(state, "", newHash);
globalHistory.pushState(undefined, "", newHash);
this.location = parseHash(newHash);
this.broadcast("PUSH");
}
}

replace(to: { pathname?: string, search?: string }, state?: S) {
replace(to: { pathname?: string, search?: string }) {
const newHash = this.fixHash(`${this.trimRightSlash(to.pathname || this.location.pathname)}${to.search || ""}`);
if (window.location.hash !== newHash) {
globalHistory.replaceState(state, "", newHash);
globalHistory.replaceState(globalHistory.state, "", newHash);
this.location = parseHash(newHash);
this.broadcast("REPLACE");
}
Expand Down Expand Up @@ -188,49 +189,49 @@ class History<S extends object = object> {
this.updateRecent();
for (const key in this._listeners) {
const listener = this._listeners[key];
listener(this.location, action);
listener({ ...this.location, state: { ...globalHistory.state } }, action);
}
}
}

export const hashHistory = new History<any>();

export function pushSearch(_: object, state?: any) {
export function pushSearch(_: object) {
const search = stringify(_ as any);
hashHistory.push({
search: search ? "?" + search : ""
}, state);
});
}

export function updateSearch(_: object, state?: any) {
export function updateSearch(_: object) {
const search = stringify(_ as any);
hashHistory.replace({
search: search ? "?" + search : ""
}, state);
});
}

export function pushUrl(_: string, state?: any) {
export function pushUrl(_: string) {
hashHistory.push({
pathname: _
}, state);
});
}

export function replaceUrl(_: string, state?: any, refresh: boolean = false) {
export function replaceUrl(_: string, refresh: boolean = false) {
hashHistory.replace({
pathname: _
}, state);
});
if (refresh) window.location.reload();
}

export function pushParam(key: string, val?: string | string[] | number | boolean, state?: any) {
pushParams({ [key]: val }, state);
export function pushParam(key: string, val?: string | string[] | number | boolean) {
pushParams({ [key]: val });
}

export function pushParamExact(key: string, val?: string | string[] | number | boolean, state?: any) {
pushParams({ [key]: val }, state, true);
export function pushParamExact(key: string, val?: string | string[] | number | boolean) {
pushParams({ [key]: val }, true);
}

export function pushParams(search: { [key: string]: string | string[] | number | boolean }, state?: any, keepEmpty: boolean = false) {
export function pushParams(search: { [key: string]: string | string[] | number | boolean }, keepEmpty: boolean = false) {
const params = parseQuery(hashHistory.location.search);
for (const key in search) {
const val = search[key];
Expand All @@ -241,15 +242,25 @@ export function pushParams(search: { [key: string]: string | string[] | number |
params[key] = val;
}
}
pushSearch(params, state);
pushSearch(params);
}

export function updateParam(key: string, val?: string | string[] | number | boolean, state?: any) {
export function updateParam(key: string, val?: string | string[] | number | boolean) {
const params = parseQuery(hashHistory.location.search);
if (val === undefined) {
delete params[key];
} else {
params[key] = val;
}
updateSearch(params, state);
updateSearch(params);
}

export function updateState(key: string, val?: string | string[] | number | boolean) {
const state = { ...globalHistory.state };
if (val === undefined) {
delete state[key];
} else {
state[key] = val;
}
globalHistory.replaceState(state, "");
}

0 comments on commit 6b4398a

Please sign in to comment.