diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index ab352361fe..a6ed57b7c1 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -190,7 +190,7 @@ export class ItemListLayout extends React.Component { } @computed get items() { - const {filters, filterCallbacks } = this; + const { filters, filterCallbacks } = this; const filterGroups = groupBy(filters, ({ type }) => type); const filterItems: ItemsFilter[] = []; @@ -212,7 +212,7 @@ export class ItemListLayout extends React.Component { 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 { { 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 ; + return ( + + ); } }) } @@ -381,7 +388,7 @@ export class ItemListLayout extends React.Component { } 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 { )} {renderTableHeader.map((cellProps, index) => { if (!this.isHiddenColumn(cellProps)) { - return ; + return ( + + ); } })} @@ -413,7 +426,7 @@ export class ItemListLayout extends React.Component { 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 { )} {isReady && ( and there is not enough space to show all the content &.sorting.nowrap { display: flex; diff --git a/src/renderer/components/table/table-cell.tsx b/src/renderer/components/table/table-cell.tsx index 81e2f9f85f..19a8ab0ea2 100644 --- a/src/renderer/components/table/table-cell.tsx +++ b/src/renderer/components/table/table-cell.tsx @@ -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; export interface TableCellProps extends React.DOMAttributes { - 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 { _sorting?: Partial; //
sorting state, don't use this prop outside (!) _sort?(sortBy: TableSortBy): void; //
sort function, don't use this prop outside (!) _nowrap?: boolean; // indicator, might come from parent , don't use this prop outside (!) + style?: React.CSSProperties; } +@observer export class TableCell extends React.Component { + private elem: HTMLElement; + + static defaultProps: TableCellProps = { + isResizable: true, + }; + @autobind() onClick(evt: React.MouseEvent) { if (this.props.onClick) { @@ -60,24 +86,87 @@ export class TableCell extends React.Component { const showCheckbox = isChecked !== undefined; if (checkbox && showCheckbox) { - return ; + return ; } } + @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 = 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 ( -
+
{this.renderCheckbox()} {_nowrap ?
{content}
: content} {this.renderSortIcon()} + {this.isResizable && ( + this.columnSize} + onStart={() => this.isResizing = true} + onEnd={() => this.isResizing = false} + onDrag={this.onResize} + /> + )}
); } diff --git a/src/renderer/components/table/table.storage.ts b/src/renderer/components/table/table.storage.ts new file mode 100644 index 0000000000..3631716fa2 --- /dev/null +++ b/src/renderer/components/table/table.storage.ts @@ -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("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; + }); +} diff --git a/src/renderer/components/table/table.tsx b/src/renderer/components/table/table.tsx index 19bd5b03e5..60094e930e 100644 --- a/src/renderer/components/table/table.tsx +++ b/src/renderer/components/table/table.tsx @@ -24,7 +24,10 @@ export interface TableProps extends React.DOMAttributes { 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 * @sortItem argument in the callback is an object, provided in