Lens app source code (#119)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2020-03-15 09:52:02 +02:00
1 parent 936cbd53d4
commit 1d0815abd2
797 files changed
+73714 -215

No files matched your search

@@ -0,0 +1,5 @@
export * from './table'
export * from './table-head'
export * from './table-row'
export * from './table-cell'
@@ -0,0 +1,69 @@
.TableCell {
padding: $padding;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
&.checkbox {
display: flex;
flex: 0 0 32px !important;
padding-left: 0;
padding-right: 0;
&:not(.disabled) {
cursor: pointer;
}
> .Checkbox {
margin: 1px auto;
}
}
// 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;
align-items: center;
> * {
flex-shrink: 0;
}
> .content {
flex-shrink: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .sortIcon {
margin: 0;
}
}
.Table.sortable > .TableHead & {
user-select: none;
white-space: nowrap;
&.sorting {
cursor: pointer;
}
.sortIcon {
transition: 350ms opacity;
opacity: .3;
&.enabled {
opacity: 1;
}
}
}
a {
color: $primary;
transition: 150ms color;
&:hover {
color: $textColorAccent;
}
}
}
@@ -0,0 +1,75 @@
import "./table-cell.scss";
import React, { ReactNode } from "react";
import { autobind, cssNames } from "../../utils";
import { SortBy, SortParams } from "./table";
import { Icon } from "../icon";
import { Checkbox } from "../checkbox";
export type TableCellElem = React.ReactElement<TableCellProps>;
export interface TableCellProps extends React.DOMAttributes<HTMLDivElement> {
className?: string;
title?: ReactNode;
checkbox?: boolean; // render cell with a checkbox
isChecked?: boolean; // mark checkbox as checked or not
sortBy?: SortBy; // column name, must be same as key in sortable object <Table sortable={}/>
_sorting?: Partial<SortParams>; // <Table> sorting state, don't use this prop outside (!)
_sort?(sortBy: SortBy): void; // <Table> sort function, don't use this prop outside (!)
_nowrap?: boolean; // indicator, might come from parent <TableHead>, don't use this prop outside (!)
}
export class TableCell extends React.Component<TableCellProps> {
@autobind()
onClick(evt: React.MouseEvent<HTMLDivElement>) {
if (this.props.onClick) {
this.props.onClick(evt);
}
if (this.isSortable) {
this.props._sort(this.props.sortBy);
}
}
get isSortable() {
const { _sorting, sortBy } = this.props;
return _sorting && sortBy !== undefined;
}
renderSortIcon() {
const { sortBy, _sorting } = this.props;
if (!this.isSortable) return;
const sortActive = _sorting.sortBy === sortBy;
const sortIconName = (!sortActive || _sorting.orderBy === "desc") ? "arrow_drop_down" : "arrow_drop_up";
return (
<Icon
className={cssNames("sortIcon", { enabled: sortActive })}
material={sortIconName}
/>
)
}
renderCheckbox() {
const { checkbox, isChecked } = this.props;
const showCheckbox = isChecked !== undefined;
if (checkbox && showCheckbox) {
return <Checkbox value={isChecked}/>
}
}
render() {
const { className, checkbox, isChecked, sortBy, _sort, _sorting, _nowrap, children, title, ...cellProps } = this.props;
const classNames = cssNames("TableCell", className, {
checkbox: checkbox,
nowrap: _nowrap,
sorting: this.isSortable,
});
const content = title || children;
return (
<div {...cellProps} className={classNames} onClick={this.onClick}>
{this.renderCheckbox()}
{_nowrap ? <div className="content">{content}</div> : content}
{this.renderSortIcon()}
</div>
)
}
}
@@ -0,0 +1,37 @@
.TableHead {
$border: 1px solid $layoutBackground;
background-color: $tableHeaderBackground;
border-bottom-width: $tableHeaderBorderWidth;
border-bottom-style: solid;
border-bottom-color: $tableHeaderBorderColor;
color: $tableHeaderColor;
display: flex;
flex-shrink: 0;
&.topLine {
border-top: $border;
}
&.sticky {
position: -webkit-sticky; // safari
position: sticky;
z-index: 1;
top: 0; // turn on sticky behaviour
}
&.nowrap {
white-space: nowrap;
}
.TableCell {
display: flex;
align-items: center;
&.checkbox {
> .Checkbox {
margin: auto;
}
}
}
}
@@ -0,0 +1,32 @@
import "./table-head.scss";
import * as React from "react";
import { cssNames } from "../../utils";
export type TableHeadElem = React.ReactElement<TableHeadProps>;
export interface TableHeadProps extends React.DOMAttributes<HTMLDivElement> {
className?: string;
showTopLine?: boolean; // show border line at the top
sticky?: boolean; // keep header on top when scrolling
nowrap?: boolean; // white-space: nowrap, align inner <TableCell> in one line
}
export class TableHead extends React.Component<TableHeadProps> {
static defaultProps: TableHeadProps = {
sticky: true,
}
render() {
const { className, sticky, nowrap, showTopLine, children, ...headProps } = this.props;
const classNames = cssNames("TableHead", className, {
sticky, nowrap,
topLine: showTopLine,
});
return (
<div className={classNames} {...headProps}>
{children}
</div>
)
}
}
@@ -0,0 +1,14 @@
.TableRow {
position: relative;
display: flex;
flex-shrink: 0;
&.disabled {
opacity: .5;
pointer-events: none;
}
&.nowrap {
white-space: nowrap;
}
}
@@ -0,0 +1,29 @@
import "./table-row.scss";
import React, { CSSProperties } from "react";
import { cssNames } from "../../utils";
import { ItemObject } from "../../item.store";
export type TableRowElem = React.ReactElement<TableRowProps>;
export interface TableRowProps extends React.DOMAttributes<HTMLDivElement> {
className?: string;
selected?: boolean;
style?: CSSProperties;
nowrap?: boolean; // white-space: nowrap, align inner <TableCell> in one line
sortItem?: ItemObject | any; // data for sorting callback in <Table sortable={}/>
searchItem?: ItemObject | any; // data for searching filters in <Table searchable={}/>
disabled?: boolean;
}
export class TableRow extends React.Component<TableRowProps> {
render() {
const { className, nowrap, selected, disabled, children, sortItem, searchItem, ...rowProps } = this.props;
const classNames = cssNames("TableRow", className, { selected, nowrap, disabled });
return (
<div className={classNames} {...rowProps}>
{children}
</div>
)
}
}
@@ -0,0 +1,35 @@
@mixin table-cell-action {
display: flex;
padding: 0 !important;
flex: 0 0 55px !important;
> * {
margin: floor($margin / 1.3) auto;
}
}
@mixin table-cell-warning {
display: flex;
align-items: center;
flex-grow: 0.3;
padding: 0;
}
@mixin table-cell-labels {
.TableRow {
.TableCell {
&.labels {
@include table-cell-labels-offsets;
}
}
}
}
@mixin table-cell-labels-offsets {
padding-top: $padding / 2;
padding-bottom: 0;
.Badge + .Badge {
margin-left: $padding / 2;
}
}
@@ -0,0 +1,53 @@
.Table {
&.scrollable {
&.light {
@include custom-scrollbar(dark);
}
@include custom-scrollbar();
flex: 1 0 0; // hackfix: flex-basis must be "0" for proper work in firefox
}
&.autoSize {
.TableCell {
flex: 1 0;
}
}
&.selectable {
.TableHead, .TableRow {
padding: 0 $padding;
}
.TableRow {
cursor: pointer;
&:hover {
background-color: $tableBgcSelected !important;
}
&.selected {
background-color: $tableBgcSelected;
color: $tableSelectedRowColor;
&:before {
content: "";
background: $primary;
position: absolute;
left: 0;
top: 0;
width: 4px;
height: 100%;
}
}
}
}
&.striped {
.TableRow {
&:nth-child(odd) {
background-color: $tableBgcStripe;
}
}
}
}
+184
View File
@@ -0,0 +1,184 @@
import "./table.scss";
import * as React from "react";
import { observer } from "mobx-react";
import { computed, observable } from "mobx";
import { autobind, cssNames, noop } from "../../utils";
import { TableRow, TableRowElem, TableRowProps } from "./table-row";
import { TableHead, TableHeadElem, TableHeadProps } from "./table-head";
import { TableCellElem } from "./table-cell";
import { VirtualList } from "../virtual-list";
import { navigation, setQueryParams } from "../../navigation";
import orderBy from "lodash/orderBy"
import { ItemObject } from "../../item.store";
// todo: refactor + decouple search from location
export type SortBy = string;
export type OrderBy = "asc" | "desc" | string;
export type SortParams = { sortBy: SortBy; orderBy: OrderBy }
export type SortingCallback<D = any> = (data: D) => string | number | (string | number)[];
export interface TableProps extends React.DOMAttributes<HTMLDivElement> {
items?: ItemObject[]; // Raw items data
className?: string;
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
sortable?: {
// 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}/>
[sortBy: string]: SortingCallback;
};
sortSyncWithUrl?: boolean; // sorting state is managed globally from url params
sortByDefault?: Partial<SortParams>; // default sorting params
onSort?: (params: SortParams) => void; // callback on sort change, default: global sync with url
noItems?: React.ReactNode; // Show no items state table list is empty
selectedItemId?: string; // Allows to scroll list to selected item
virtual?: boolean; // Use virtual list component to render only visible rows
rowPadding?: string;
rowLineHeight?: string;
customRowHeights?: (item: object, lineHeight: number, paddings: number) => number;
getTableRow?: (uid: string) => React.ReactElement<TableRowProps>;
}
@observer
export class Table extends React.Component<TableProps> {
static defaultProps: TableProps = {
scrollable: true,
autoSize: true,
rowPadding: "8px",
rowLineHeight: "17px",
sortSyncWithUrl: true,
}
@observable sortParamsLocal = this.props.sortByDefault;
@computed get sortParams(): Partial<SortParams> {
if (this.props.sortSyncWithUrl) {
const sortBy = navigation.searchParams.get("sortBy")
const orderBy = navigation.searchParams.get("orderBy")
return { sortBy, orderBy };
}
return this.sortParamsLocal || {};
}
renderHead() {
const { sortable, children } = this.props;
const content = React.Children.toArray(children) as (TableRowElem | TableHeadElem)[];
const headElem: React.ReactElement<TableHeadProps> = content.find(elem => elem.type === TableHead);
if (headElem) {
if (sortable) {
const columns = React.Children.toArray(headElem.props.children) as TableCellElem[];
return React.cloneElement(headElem, {
children: columns.map(elem => {
if (elem.props.checkbox) {
return elem;
}
const title = elem.props.title || (
// copy cell content to title if it's a string
// usable if part of TableCell's content is hidden when there is not enough space
typeof elem.props.children === "string" ? elem.props.children : undefined
);
return React.cloneElement(elem, {
title: title,
_sort: this.sort,
_sorting: this.sortParams,
_nowrap: headElem.props.nowrap,
})
})
});
}
return headElem;
}
}
getSorted(items: any[]) {
const { sortParams } = this;
const sortingCallback = this.props.sortable[sortParams.sortBy] || noop;
return orderBy(
items,
sortingCallback,
sortParams.orderBy as any
);
}
@autobind()
protected onSort(params: SortParams) {
const { sortSyncWithUrl, onSort } = this.props;
if (sortSyncWithUrl) {
setQueryParams(params)
}
else {
this.sortParamsLocal = params;
}
if (onSort) {
onSort(params)
}
}
@autobind()
sort(colName: SortBy) {
const { sortBy, orderBy } = this.sortParams;
const sameColumn = sortBy == colName;
const newSortBy: SortBy = colName;
const newOrderBy: OrderBy = (!orderBy || !sameColumn || orderBy === "desc") ? "asc" : "desc";
this.onSort({
sortBy: String(newSortBy),
orderBy: newOrderBy,
});
}
renderRows() {
const { sortable, noItems, children, virtual, customRowHeights, rowLineHeight, rowPadding, items, getTableRow, selectedItemId, className } = this.props;
const content = React.Children.toArray(children) as (TableRowElem | TableHeadElem)[];
let rows: React.ReactElement<TableRowProps>[] = content.filter(elem => elem.type === TableRow);
let sortedItems = rows.length ? rows.map(row => row.props.sortItem) : [...items];
if (sortable) {
sortedItems = this.getSorted(sortedItems);
if (rows.length) {
rows = sortedItems.map(item => rows.find(row => {
return item == row.props.sortItem
}))
}
}
if (!rows.length && !items.length && noItems) {
return noItems;
}
if (virtual) {
const lineHeight = parseFloat(rowLineHeight);
const padding = parseFloat(rowPadding);
let rowHeights: number[] = Array(items.length).fill(lineHeight + padding * 2);
if (customRowHeights) {
rowHeights = sortedItems.map(item => {
return customRowHeights(item, lineHeight, padding * 2);
});
}
return (
<VirtualList
items={sortedItems}
rowHeights={rowHeights}
getTableRow={getTableRow}
selectedItemId={selectedItemId}
className={className}
/>
);
}
return rows;
}
render() {
const { selectable, scrollable, sortable, autoSize, virtual } = this.props;
let { className } = this.props;
className = cssNames("Table flex column", className, {
selectable, scrollable, sortable, autoSize, virtual,
});
return (
<div className={className}>
{this.renderHead()}
{this.renderRows()}
</div>
)
}
}