1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

allow to resize & restore table column sizes after reload

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-03-25 18:31:42 +02:00
parent 0765bcee9c
commit 85ddcb47b4
5 changed files with 156 additions and 12 deletions

View File

@ -190,7 +190,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}
@computed get items() {
const {filters, filterCallbacks } = this;
const { filters, filterCallbacks } = this;
const filterGroups = groupBy<Filter>(filters, ({ type }) => type);
const filterItems: ItemsFilter[] = [];
@ -212,7 +212,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
getRow(uid: string) {
const {
isSelectable, renderTableHeader, renderTableContents, renderItemMenu,
store, hasDetailsView, onDetails,
store, hasDetailsView, onDetails, tableId,
copyClassNameFromHeadCells, customizeTableRowProps, detailsItem,
} = this.props;
const { isSelected } = store;
@ -241,14 +241,21 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
{
renderTableContents(item).map((content, index) => {
const cellProps: TableCellProps = isReactNode(content) ? { children: content } : content;
const headCell = renderTableHeader?.[index];
const headCell: TableCellProps = renderTableHeader?.[index];
if (copyClassNameFromHeadCells && headCell) {
cellProps.className = cssNames(cellProps.className, headCell.className);
}
if (!headCell || !this.isHiddenColumn(headCell)) {
return <TableCell key={index} {...cellProps} />;
return (
<TableCell
key={index}
tableId={tableId}
storageId={headCell.id ?? headCell.storageId}
{...cellProps}
/>
);
}
})
}
@ -381,7 +388,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}
renderTableHeader() {
const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store } = this.props;
const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store, tableId } = this.props;
if (!renderTableHeader) {
return;
@ -400,7 +407,13 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
)}
{renderTableHeader.map((cellProps, index) => {
if (!this.isHiddenColumn(cellProps)) {
return <TableCell key={cellProps.id ?? index} {...cellProps} />;
return (
<TableCell
key={cellProps.id ?? cellProps.storageId ?? index}
storageId={cellProps.storageId}
tableId={tableId} {...cellProps}
/>
);
}
})}
<TableCell className="menu">
@ -413,7 +426,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
renderList() {
const {
store, hasDetailsView, addRemoveButtons = {}, virtual, sortingCallbacks, detailsItem,
tableProps = {},
tableId, tableProps = {},
} = this.props;
const { isReady, removeItemsDialog, items } = this;
const { selectedItems } = store;
@ -426,6 +439,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
)}
{isReady && (
<Table
storageKey={tableId}
virtual={virtual}
selectable={hasDetailsView}
sortable={sortingCallbacks}

View File

@ -19,6 +19,15 @@
}
}
&.resizable {
position: relative; // required for resizing-anchor with `position:absolute`
&.resizing {
filter: invert(50%);
pointer-events: none;
}
}
// fix: don't hide "sortIcon" when used with <TableHead nowrap> and there is not enough space to show all the content
&.sorting.nowrap {
display: flex;

View File

@ -2,16 +2,34 @@ import "./table-cell.scss";
import type { TableSortBy, TableSortParams } from "./table";
import React, { ReactNode } from "react";
import { computed, observable } from "mobx";
import { observer } from "mobx-react";
import { autobind, cssNames, displayBooleans } from "../../utils";
import { Icon } from "../icon";
import { Checkbox } from "../checkbox";
import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor";
import { getColumnSize, setColumnSize } from "./table.storage";
export type TableCellElem = React.ReactElement<TableCellProps>;
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
id?: string; // used for configuration visibility of columns
id?: string;
/**
* Used to persist configuration of table: column size, visibility, etc.
* Required with props.isResizable={true}
*/
storageId?: string;
/**
* Parent table's props.tableId, required with props.storageId
*/
tableId?: string;
className?: string;
title?: ReactNode;
/**
* Allow to resize width and save to local storage, default: true
* (applicable only with props.id)
*/
isResizable?: boolean;
checkbox?: boolean; // render cell with a checkbox
isChecked?: boolean; // mark checkbox as checked or not
renderBoolean?: boolean; // show "true" or "false" for all of the children elements are "typeof boolean"
@ -20,9 +38,17 @@ export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
_sorting?: Partial<TableSortParams>; // <Table> sorting state, don't use this prop outside (!)
_sort?(sortBy: TableSortBy): void; // <Table> sort function, don't use this prop outside (!)
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
style?: React.CSSProperties;
}
@observer
export class TableCell extends React.Component<TableCellProps> {
private elem: HTMLElement;
static defaultProps: TableCellProps = {
isResizable: true,
};
@autobind()
onClick(evt: React.MouseEvent<HTMLDivElement>) {
if (this.props.onClick) {
@ -60,24 +86,87 @@ export class TableCell extends React.Component<TableCellProps> {
const showCheckbox = isChecked !== undefined;
if (checkbox && showCheckbox) {
return <Checkbox value={isChecked} />;
return <Checkbox value={isChecked}/>;
}
}
@observable isResizing = false;
@computed get columnId(): string {
return this.props.id ?? this.props.storageId;
}
@computed get columnSize(): number {
return getColumnSize(this.props.tableId, this.columnId) ?? 0;
}
@computed get isResizable(): boolean {
return [
this.props.isResizable,
this.props.tableId,
this.columnId,
].every(Boolean);
}
@computed get style(): React.CSSProperties {
const styles: React.CSSProperties & Record<string, any> = Object.assign({}, this.props.style);
if (this.isResizable && this.columnSize) {
styles.flexGrow = 0;
styles.flexBasis = this.columnSize;
}
return styles;
}
@autobind()
onResize(extent: number) {
const { tableId } = this.props;
const { columnId } = this;
const size = extent || this.elem?.offsetWidth;
// persist state in storage
setColumnSize({ tableId, columnId, size });
}
@autobind()
bindRef(ref: HTMLElement) {
this.elem = ref;
}
render() {
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, renderBoolean: displayBoolean, showWithColumn, ...cellProps } = this.props;
const {
className, checkbox, isChecked, isResizable, sortBy,
_sort, _sorting, _nowrap, children, title, tableId, storageId,
renderBoolean: displayBoolean, showWithColumn,
...cellProps
} = this.props;
const classNames = cssNames("TableCell", className, {
checkbox,
nowrap: _nowrap,
sorting: this.isSortable,
resizing: this.isResizing,
resizable: isResizable,
});
const content = displayBooleans(displayBoolean, title || children);
return (
<div {...cellProps} className={classNames} onClick={this.onClick}>
<div {...cellProps} className={classNames} style={this.style} onClick={this.onClick} ref={this.bindRef}>
{this.renderCheckbox()}
{_nowrap ? <div className="content">{content}</div> : content}
{this.renderSortIcon()}
{this.isResizable && (
<ResizingAnchor
direction={ResizeDirection.HORIZONTAL}
placement={ResizeSide.TRAILING}
growthDirection={ResizeGrowthDirection.LEFT_TO_RIGHT}
getCurrentExtent={() => this.columnSize}
onStart={() => this.isResizing = true}
onEnd={() => this.isResizing = false}
onDrag={this.onResize}
/>
)}
</div>
);
}

View File

@ -0,0 +1,29 @@
import { createStorage } from "../../utils";
// TODO: move here `hiddenTableColumns` from user-store.ts
export interface TableStorageModel {
columnSizes: {
[tableId: string]: {
[columnId: string]: number;
}
}
}
export const tableStorage = createStorage<TableStorageModel>("table_settings", {
columnSizes: {},
});
export function getColumnSize(tableId: string, columnId: string): number | undefined {
return tableStorage.get().columnSizes[tableId]?.[columnId];
}
export function setColumnSize(params: { tableId: string, columnId: string, size: number }) {
const { tableId, columnId, size } = params;
if (!tableId || !columnId) return;
tableStorage.merge(draft => {
draft.columnSizes[tableId] ??= {};
draft.columnSizes[tableId][columnId] = size;
});
}

View File

@ -24,7 +24,10 @@ export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
autoSize?: boolean; // Setup auto-sizing for all columns (flex: 1 0)
selectable?: boolean; // Highlight rows on hover
scrollable?: boolean; // Use scrollbar if content is bigger than parent's height
storageKey?: string; // Keep some data in localStorage & restore on page reload, e.g sorting params
/**
* Unique key for the storage to keep table' user settings and restore on page reload
*/
storageKey?: string;
/**
* Define sortable callbacks for every column in <TableHead><TableCell sortBy="someCol"><TableHead>
* @sortItem argument in the callback is an object, provided in <TableRow sortItem={someColDataItem}/>