diff options
author | 2019-02-11 14:54:12 +0100 | |
---|---|---|
committer | 2019-02-11 14:54:53 +0100 | |
commit | 3d202a04b99f0e61b6ccf8b7a5610e1a15ca58e7 (patch) | |
tree | ab756cfa8de5eced886d3947423d198be8c0ce62 /sdnr/wt/odlux/framework/src/components/material-table | |
parent | 12a8c669f52c0e84d580c078cee849b25133b585 (diff) |
Add sdnr wt odlux
Add complete sdnr wireless transport app odlux core and apps
Change-Id: I5dcbfb8f3b790e3bda7c8df67bd69d81958f65e5
Issue-ID: SDNC-576
Signed-off-by: Herbert Eiselt <herbert.eiselt@highstreet-technologies.com>
Diffstat (limited to 'sdnr/wt/odlux/framework/src/components/material-table')
6 files changed, 939 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts b/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts new file mode 100644 index 000000000..6acea01d5 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts @@ -0,0 +1,27 @@ +
+import * as React from 'react';
+
+export enum ColumnType {
+ text,
+ numeric,
+ custom
+}
+
+type CustomControl<TData> = {
+ rowData: TData
+}
+
+export type ColumnModel<TData> = {
+ title?: string;
+ disablePadding?: boolean;
+ width?: string | number;
+ disableSorting?: boolean;
+ disableFilter?: boolean;
+} & ({
+ property: string;
+ type: ColumnType.custom;
+ customControl: React.ComponentType<CustomControl<TData>>;
+} | {
+ property: keyof TData;
+ type?: ColumnType.numeric | ColumnType.text;
+});
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx new file mode 100644 index 000000000..3b906cfbb --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx @@ -0,0 +1,423 @@ +import * as React from 'react';
+import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles';
+
+import Table from '@material-ui/core/Table';
+import TableBody from '@material-ui/core/TableBody';
+import TableCell from '@material-ui/core/TableCell';
+import TablePagination from '@material-ui/core/TablePagination';
+import TableRow from '@material-ui/core/TableRow';
+import Paper from '@material-ui/core/Paper';
+import Checkbox from '@material-ui/core/Checkbox';
+
+import { TableToolbar } from './tableToolbar';
+import { EnhancedTableHead } from './tableHead';
+import { EnhancedTableFilter } from './tableFilter';
+
+import { ColumnModel, ColumnType } from './columnModel';
+import { Omit } from '@material-ui/core';
+import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon';
+export { ColumnModel, ColumnType } from './columnModel';
+
+type propType = string | number | null | undefined | (string|number)[];
+type dataType = { [prop: string]: propType };
+type resultType<TData = dataType> = { page: number, rowCount: number, rows: TData[] };
+
+export type DataCallback<TData = dataType> = (page?: number, rowsPerPage?: number, orderBy?: string | null, order?: 'asc' | 'desc' | null, filter?: { [property: string]: string }) =>resultType<TData> | Promise<resultType<TData>>;
+
+function desc(a: dataType, b: dataType, orderBy: string) {
+ if ((b[orderBy] || "") < (a[orderBy] || "") ) {
+ return -1;
+ }
+ if ((b[orderBy] || "") > (a[orderBy] || "") ) {
+ return 1;
+ }
+ return 0;
+}
+
+function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) {
+ const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][];
+ stabilizedThis.sort((a, b) => {
+ const order = cmp(a[0], b[0]);
+ if (order !== 0) return order;
+ return a[1] - b[1];
+ });
+ return stabilizedThis.map(el => el[0]);
+}
+
+function getSorting(order: 'asc' | 'desc' | null, orderBy: string) {
+ return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy);
+}
+
+const styles = (theme: Theme) => createStyles({
+ root: {
+ width: '100%',
+ marginTop: theme.spacing.unit * 3,
+ },
+ table: {
+ minWidth: 1020,
+ },
+ tableWrapper: {
+ overflowX: 'auto',
+ },
+});
+
+export type MaterialTableComponentState<TData = {}> = {
+ order: 'asc' | 'desc';
+ orderBy: string | null;
+ selected: any[] | null;
+ rows: TData[];
+ rowCount: number;
+ page: number;
+ rowsPerPage: number;
+ loading: boolean;
+ showFilter: boolean;
+ filter: { [property: string]: string };
+};
+
+export type TableApi = { forceRefresh?: () => Promise<void> };
+
+type MaterialTableComponentBaseProps<TData> = WithStyles<typeof styles> & {
+ columns: ColumnModel<TData>[];
+ idProperty: keyof TData | ((data: TData) => React.Key );
+ title?: string;
+ enableSelection?: boolean;
+ disableSorting?: boolean;
+ disableFilter?: boolean;
+ customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void }[];
+ onHandleClick?(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData): void;
+};
+
+type MaterialTableComponentPropsWithRows<TData={}> = MaterialTableComponentBaseProps<TData> & { rows: TData[]; asynchronus?: boolean; };
+type MaterialTableComponentPropsWithRequestData<TData={}> = MaterialTableComponentBaseProps<TData> & { onRequestData: DataCallback; tableApi?: TableApi; };
+type MaterialTableComponentPropsWithExternalState<TData={}> = MaterialTableComponentBaseProps<TData> & MaterialTableComponentState & {
+ onToggleFilter: () => void;
+ onFilterChanged: (property: string, filterTerm: string) => void;
+ onHandleChangePage: (page: number) => void;
+ onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void;
+ onHandleRequestSort: (property: string) => void;
+};
+
+type MaterialTableComponentProps<TData = {}> =
+ MaterialTableComponentPropsWithRows<TData> |
+ MaterialTableComponentPropsWithRequestData<TData> |
+ MaterialTableComponentPropsWithExternalState<TData>;
+
+function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows {
+ return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array;
+}
+
+function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData {
+ return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function;
+}
+
+function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState {
+ const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState)
+ return propsWithExternalState.onFilterChanged instanceof Function ||
+ propsWithExternalState.onHandleChangePage instanceof Function ||
+ propsWithExternalState.onHandleChangeRowsPerPage instanceof Function ||
+ propsWithExternalState.onToggleFilter instanceof Function ||
+ propsWithExternalState.onHandleRequestSort instanceof Function
+}
+
+class MaterialTableComponent<TData extends {} = {}> extends React.Component<MaterialTableComponentProps, MaterialTableComponentState> {
+
+ constructor(props: MaterialTableComponentProps) {
+ super(props);
+
+ const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0;
+ const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10;
+
+ this.state = {
+ filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {},
+ showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false,
+ loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false,
+ order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : 'asc',
+ orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : null,
+ selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null,
+ rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [],
+ rowCount: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0,
+ page,
+ rowsPerPage,
+ };
+
+ if (isMaterialTableComponentPropsWithRequestData(this.props)) {
+ this.update();
+
+ if (this.props.tableApi) {
+ this.props.tableApi.forceRefresh = () => this.update();
+ }
+ }
+ }
+ render(): JSX.Element {
+ const { classes, columns } = this.props;
+ const { rows, rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state;
+ const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage);
+ const getId = typeof this.props.idProperty !== "function" ? (data: TData) => ((data as {[key:string]: any })[this.props.idProperty as any as string] as string | number) : this.props.idProperty;
+ const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) }
+ return (
+ <Paper className={ classes.root }>
+ <TableToolbar numSelected={ selected && selected.length } title={ this.props.title } customActionButtons={ this.props.customActionButtons } onExportToCsv={ this.exportToCsv }
+ onToggleFilter={ toggleFilter } />
+ <div className={ classes.tableWrapper }>
+ <Table className={ classes.table } aria-labelledby="tableTitle">
+ <EnhancedTableHead
+ columns={ columns }
+ numSelected={ selected && selected.length }
+ order={ order }
+ orderBy={ orderBy }
+ onSelectAllClick={ this.handleSelectAllClick }
+ onRequestSort={ this.onHandleRequestSort }
+ rowCount={ rows.length }
+ enableSelection={ this.props.enableSelection }
+ />
+ <TableBody>
+ { showFilter && <EnhancedTableFilter columns={ columns } filter={ filter } onFilterChanged={ this.onFilterChanged } enableSelection={this.props.enableSelection} /> || null }
+ { rows // may need ordering here
+ .map((entry: TData & { [key: string]: any }) => {
+ const entryId = getId(entry);
+ const isSelected = this.isSelected(entryId);
+ return (
+ <TableRow
+ hover
+ onClick={ event => this.handleClick(event, entry, entryId) }
+ role="checkbox"
+ aria-checked={ isSelected }
+ tabIndex={ -1 }
+ key={ entryId }
+ selected={ isSelected }
+ >
+ { this.props.enableSelection
+ ? <TableCell padding="checkbox" style={ { width: "50px" } }>
+ <Checkbox checked={ isSelected } />
+ </TableCell>
+ : null
+ }
+ {
+ this.props.columns.map(
+ col => {
+ const style = col.width ? { width: col.width } : {};
+ return (
+ <TableCell key={ col.property } align={ col.type === ColumnType.numeric ? 'right' : 'left' } style={ style }>
+ { col.type === ColumnType.custom && col.customControl
+ ? <col.customControl rowData={ entry } />
+ : entry[col.property]
+ }
+ </TableCell>
+ );
+ }
+ )
+ }
+ </TableRow>
+ );
+ }) }
+ { emptyRows > 0 && (
+ <TableRow style={ { height: 49 * emptyRows } }>
+ <TableCell colSpan={ this.props.columns.length } />
+ </TableRow>
+ ) }
+ </TableBody>
+ </Table>
+ </div>
+ <TablePagination
+ rowsPerPageOptions={ [5, 10, 25] }
+ component="div"
+ count={ rowCount }
+ rowsPerPage={ rowsPerPage }
+ page={ page }
+ backIconButtonProps={ {
+ 'aria-label': 'Previous Page',
+ } }
+ nextIconButtonProps={ {
+ 'aria-label': 'Next Page',
+ } }
+ onChangePage={ this.onHandleChangePage }
+ onChangeRowsPerPage={ this.onHandleChangeRowsPerPage }
+ />
+ </Paper>
+ );
+ }
+
+ static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } {
+ if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) {
+ return {
+ ...state,
+ rows: props.rows,
+ rowCount: props.rowCount,
+ orderBy: props.orderBy,
+ order: props.order,
+ filter: props.filter,
+ loading: props.loading,
+ showFilter: props.showFilter,
+ page: props.page,
+ rowsPerPage: props.rowsPerPage
+ }
+ } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) {
+ const newState = MaterialTableComponent.updateRows(props, state);
+ return {
+ ...state,
+ ...newState,
+ _rawRows: props.rows || []
+ };
+ }
+ return state;
+ }
+
+ private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], rowCount: number } {
+ try {
+ const { page, rowsPerPage, order, orderBy, filter } = state;
+ let data: dataType[] = props.rows || [];
+ let filtered = false;
+ if (state.showFilter) {
+ Object.keys(filter).forEach(prop => {
+ const exp = filter[prop];
+ filtered = filtered || !!exp;
+ data = exp ? data.filter((val) => {
+ const value = val[prop];
+ return value && value.toString().indexOf(exp) > -1;
+ }) : data;
+ });
+ }
+
+ const rowCount = data.length;
+
+ data = (orderBy && order
+ ? stableSort(data, getSorting(order, orderBy))
+ : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage);
+
+ return {
+ rows: data,
+ rowCount
+ };
+ } catch{
+ return {
+ rows: [],
+ rowCount: 0
+ }
+ }
+ }
+
+ private async update() {
+ if (isMaterialTableComponentPropsWithRequestData(this.props)) {
+ const response = await Promise.resolve(
+ this.props.onRequestData(
+ this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {})
+ );
+ this.setState(response);
+ } else {
+ this.setState(MaterialTableComponent.updateRows(this.props, this.state));
+ }
+ }
+
+ private onFilterChanged = (property: string, filterTerm: string) => {
+ if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
+ this.props.onFilterChanged(property, filterTerm);
+ return;
+ }
+ if (this.props.disableFilter) return;
+ const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
+ if (colDefinition && colDefinition.disableFilter) return;
+
+ const filter = { ...this.state.filter, [property]: filterTerm };
+ this.setState({
+ filter
+ }, this.update);
+ };
+
+ private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => {
+ if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
+ this.props.onHandleRequestSort(property);
+ return;
+ }
+ if (this.props.disableSorting) return;
+ const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property);
+ if (colDefinition && colDefinition.disableSorting) return;
+
+ const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property;
+ const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc';
+ this.setState({
+ order,
+ orderBy
+ }, this.update);
+ };
+
+ handleSelectAllClick: () => {};
+
+ private onHandleChangePage = (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
+ if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
+ this.props.onHandleChangePage(page);
+ return;
+ }
+ this.setState({
+ page
+ }, this.update);
+ };
+
+ private onHandleChangeRowsPerPage = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
+ if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) {
+ this.props.onHandleChangeRowsPerPage(+(event && event.target.value));
+ return;
+ }
+ const rowsPerPage = +(event && event.target.value);
+ if (rowsPerPage && rowsPerPage > 0) {
+ this.setState({
+ rowsPerPage
+ }, this.update);
+ }
+ };
+
+ private isSelected(id: string | number): boolean {
+ let selected = this.state.selected || [];
+ const selectedIndex = selected.indexOf(id);
+ return (selectedIndex > -1);
+ }
+
+ private handleClick(event: React.MouseEvent<HTMLTableRowElement>, rowData: TData, id: string | number): void {
+ if (this.props.onHandleClick instanceof Function) {
+ this.props.onHandleClick(event, rowData);
+ return;
+ }
+ if (!this.props.enableSelection){
+ return;
+ }
+ let selected = this.state.selected || [];
+ const selectedIndex = selected.indexOf(id);
+ if (selectedIndex > -1) {
+ selected = [
+ ...selected.slice(0, selectedIndex),
+ ...selected.slice(selectedIndex + 1)
+ ];
+ } else {
+ selected = [
+ ...selected,
+ id
+ ];
+ }
+ this.setState({
+ selected
+ });
+ }
+
+ private exportToCsv = () => {
+ let file;
+ const data: string[] = [];
+ data.push(this.props.columns.map(col => col.title || col.property).join(',')+"\r\n");
+ this.state.rows && this.state.rows.forEach((row : any)=> {
+ data.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n");
+ });
+ const properties = { type: 'text/csv' }; // Specify the file's mime-type.
+ try {
+ // Specify the filename using the File constructor, but ...
+ file = new File(data, "export.csv", properties);
+ } catch (e) {
+ // ... fall back to the Blob constructor if that isn't supported.
+ file = new Blob(data, properties);
+ }
+ const url = URL.createObjectURL(file);
+ window.location.replace(url);
+ }
+}
+
+export type MaterialTableCtorType<TData extends {} = {}> = new () => React.Component<Omit<MaterialTableComponentProps<TData>, 'classes'>>;
+
+export const MaterialTable = withStyles(styles)(MaterialTableComponent);
+export default MaterialTable;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx new file mode 100644 index 000000000..68e47d7ee --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx @@ -0,0 +1,67 @@ + +import * as React from 'react'; +import { ColumnModel, ColumnType } from './columnModel'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + + +import TableCell from '@material-ui/core/TableCell'; +import TableRow from '@material-ui/core/TableRow'; +import Input from '@material-ui/core/Input'; + + +const styles = (theme: Theme) => createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + input: { + margin: theme.spacing.unit, + }, +}); + +interface IEnhancedTableFilterComponentProps extends WithStyles<typeof styles> { + onFilterChanged: (property: string, filterTerm: string) => void; + filter: { [property: string]: string }; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableFilterComponent extends React.Component<IEnhancedTableFilterComponentProps> { + createFilterHandler = (property: string) => (event: React.ChangeEvent<HTMLInputElement>) => { + this.props.onFilterChanged && this.props.onFilterChanged(property, event.target.value); + }; + + render() { + const { columns, filter, classes } = this.props; + return ( + <TableRow> + { this.props.enableSelection + ? <TableCell padding="checkbox" style={ { width: "50px" } }> + </TableCell> + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + <TableCell + key={ col.property } + padding={ col.disablePadding ? 'none' : 'default' } + style={ style } + > + { col.disableFilter || (col.type === ColumnType.custom) ? null : <Input + className={ classes.input } + inputProps={ { + 'aria-label': 'Filter', + } } + value={ filter[col.property] || '' } + onChange={ this.createFilterHandler(col.property) } + /> } + </TableCell> + ); + }, this) } + </TableRow> + ); + } +} + +export const EnhancedTableFilter = withStyles(styles)(EnhancedTableFilterComponent);
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx new file mode 100644 index 000000000..5846e5e51 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx @@ -0,0 +1,84 @@ + +import * as React from 'react'; +import { ColumnModel, ColumnType } from './columnModel'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Checkbox from '@material-ui/core/Checkbox'; +import Tooltip from '@material-ui/core/Tooltip'; + +interface IEnhancedTableHeadComponentProps { + numSelected: number | null; + onRequestSort: (event: React.SyntheticEvent, property: string) => void; + onSelectAllClick: () => void; + order: 'asc' | 'desc'; + orderBy: string | null; + rowCount: number; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableHeadComponent extends React.Component<IEnhancedTableHeadComponentProps> { + createSortHandler = (property: string) => (event: React.SyntheticEvent) => { + this.props.onRequestSort(event, property); + }; + + render() { + const { onSelectAllClick, order, orderBy, numSelected, rowCount, columns } = this.props; + + return ( + <TableHead> + <TableRow> + { this.props.enableSelection + ? <TableCell padding="checkbox" style={ { width: "50px" } }> + <Checkbox + indeterminate={ numSelected && numSelected > 0 && numSelected < rowCount || undefined } + checked={ numSelected === rowCount } + onChange={ onSelectAllClick } + /> + </TableCell> + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + <TableCell + key={ col.property } + align={ col.type === ColumnType.numeric ? 'right' : 'left' } + padding={ col.disablePadding ? 'none' : 'default' } + sortDirection={ orderBy === (col.property) ? order : false } + style={ style } + > + { col.disableSorting || (col.type === ColumnType.custom) + ? <TableSortLabel + active={ false } + direction={ undefined } + > + { col.title || col.property } + </TableSortLabel> + : <Tooltip + title="Sort" + placement={ col.type === ColumnType.numeric ? 'bottom-end' : 'bottom-start' } + enterDelay={ 300 } + > + <TableSortLabel + active={ orderBy === col.property } + direction={ order || undefined } + onClick={ this.createSortHandler(col.property) } + > + { col.title || col.property } + </TableSortLabel> + </Tooltip> } + </TableCell> + ); + }, this) } + </TableRow> + </TableHead> + ); + } +} + +export const EnhancedTableHead = EnhancedTableHeadComponent;
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx new file mode 100644 index 000000000..9ee2e13c7 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx @@ -0,0 +1,131 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import DeleteIcon from '@material-ui/icons/Delete'; +import MoreIcon from '@material-ui/icons/MoreVert'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import { lighten } from '@material-ui/core/styles/colorManipulator'; +import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + root: { + paddingRight: theme.spacing.unit, + }, + highlight: + theme.palette.type === 'light' + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + spacer: { + flex: '1 1 100%', + }, + actions: { + color: theme.palette.text.secondary, + display: "flex", + flex: "auto", + flexDirection: "row" + }, + title: { + flex: '0 0 auto', + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, +}); + +interface ITableToolbarComponentProps extends WithStyles<typeof styles> { + numSelected: number | null; + title?: string; + customActionButtons?: { icon: React.ComponentType<SvgIconProps>, tooltip?: string, onClick: () => void }[]; + onToggleFilter: () => void; + onExportToCsv: () => void; +} + +class TableToolbarComponent extends React.Component<ITableToolbarComponentProps, { anchorEl: EventTarget & HTMLElement | null }> { + constructor(props: ITableToolbarComponentProps) { + super(props); + + this.state = { + anchorEl: null + }; + } + + private handleMenu = (event: React.MouseEvent<HTMLElement>) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private handleClose = () => { + this.setState({ anchorEl: null }); + }; + render() { + const { numSelected, classes } = this.props; + const open = !!this.state.anchorEl; + + return ( + <Toolbar className={ `${ classes.root } ${ numSelected && numSelected > 0 ? classes.highlight : '' } ` } > + <div className={ classes.title }> + { numSelected && numSelected > 0 ? ( + <Typography color="inherit" variant="subheading"> + { numSelected } selected + </Typography> + ) : ( + <Typography variant="headline" id="tableTitle"> + { this.props.title || null } + </Typography> + ) } + </div> + <div className={ classes.spacer } /> + <div className={ classes.actions }> + { this.props.customActionButtons + ? this.props.customActionButtons.map((action, ind) =>( + <Tooltip key={ `custom-action-${ ind }`} title={action.tooltip}> + <IconButton aria-label={ `custom-action-${ind}` } onClick={() => action.onClick() }> + <action.icon /> + </IconButton> + </Tooltip> + )) + : null } + { numSelected && numSelected > 0 ? ( + <Tooltip title="Delete"> + <IconButton aria-label="Delete"> + <DeleteIcon /> + </IconButton> + </Tooltip> + ) : ( + <Tooltip title="Filter list"> + <IconButton aria-label="Filter list" onClick={ () => { this.props.onToggleFilter && this.props.onToggleFilter() } }> + <FilterListIcon /> + </IconButton> + </Tooltip> + ) } + <Tooltip title="Actions"> + <IconButton color="inherit" + aria-owns={ open ? 'menu-appbar' : undefined } + aria-haspopup="true" + onClick={ this.handleMenu } > + <MoreIcon /> + </IconButton> + </Tooltip> + <Menu id="menu-appbar" anchorEl={ this.state.anchorEl } anchorOrigin={ { vertical: 'top', horizontal: 'right' } } + transformOrigin={ { vertical: 'top', horizontal: 'right' } } open={ open } onClose={ this.handleClose } > + <MenuItem onClick={ this.props.onExportToCsv }>Export as CSV</MenuItem> + </Menu> + </div> + </Toolbar> + ); + } +}; + +export const TableToolbar = withStyles(styles)(TableToolbarComponent);
\ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts new file mode 100644 index 000000000..e52fdb731 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts @@ -0,0 +1,207 @@ +import { Action, IActionHandler } from '../../flux/action'; +import { Dispatch } from '../../flux/store'; + +import { AddErrorInfoAction } from '../../actions/errorActions'; +import { IApplicationStoreState } from '../../store/applicationStore'; + +import { DataCallback } from "."; +export interface IExternalTableState<TData> { + order: 'asc' | 'desc'; + orderBy: string | null; + selected: any[] | null; + rows: TData[]; + rowCount: number; + page: number; + rowsPerPage: number; + loading: boolean; + showFilter: boolean; + filter: { [property: string]: string }; +} + +/** Create an actionHandler and actions for external table states. */ +export function createExternal<TData>(callback: DataCallback<TData>, selectState: (appState: IApplicationStoreState) => IExternalTableState<TData>) { + + //#region Actions + abstract class TableAction extends Action { } + + + class RequestSortAction extends TableAction { + constructor(public orderBy: string) { + super(); + } + } + + class SetSelectedAction extends TableAction { + constructor(public selected: TData[] | null) { + super(); + } + } + + class SetPageAction extends TableAction { + constructor(public page: number) { + super(); + } + } + + class SetRowsPerPageAction extends TableAction { + constructor(public rowsPerPage: number) { + super(); + } + } + + class SetFilterChangedAction extends TableAction { + constructor(public filter: {[key: string]: string}) { + super(); + } + } + + class SetShowFilterAction extends TableAction { + constructor(public show: boolean) { + super(); + } + } + + class RefreshAction extends TableAction { + constructor() { + super(); + } + } + + class SetResultAction extends TableAction { + constructor(public result: { page: number, rowCount: number, rows: TData[] }) { + super(); + } + } + + // #endregion + + //#region Action Handler + const externalTableStateInit: IExternalTableState<TData> = { + order: 'asc', + orderBy: null, + selected: null, + rows: [], + rowCount: 0, + page: 0, + rowsPerPage: 10, + loading: false, + showFilter: false, + filter: {} + }; + + const externalTableStateActionHandler: IActionHandler<IExternalTableState<TData>> = (state = externalTableStateInit, action) => { + if (!(action instanceof TableAction)) return state; + if (action instanceof RefreshAction) { + state = { + ...state, + loading: true + } + } else if (action instanceof SetResultAction) { + state = { + ...state, + loading: false, + rows: action.result.rows, + rowCount: action.result.rowCount, + page: action.result.page, + } + } else if (action instanceof RequestSortAction) { + state = { + ...state, + loading: true, + orderBy : state.orderBy === action.orderBy && state.order === 'desc' ? null : action.orderBy , + order: state.orderBy === action.orderBy && state.order === 'asc' ? 'desc' : 'asc', + } + } else if (action instanceof SetShowFilterAction) { + state = { + ...state, + loading: true, + showFilter: action.show + } + } else if (action instanceof SetFilterChangedAction) { + state = { + ...state, + loading: true, + filter: action.filter + } + } else if (action instanceof SetPageAction) { + state = { + ...state, + loading: true, + page: action.page + } + } else if (action instanceof SetRowsPerPageAction) { + state = { + ...state, + loading: true, + rowsPerPage: action.rowsPerPage + } + } + return state; + } + + //const createTableAction(tableAction) + + //#endregion + const reloadAction = (dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + dispatch(new RefreshAction()); + const ownState = selectState(getAppState()); + Promise.resolve(callback(ownState.page, ownState.rowsPerPage, ownState.orderBy, ownState.order, ownState.showFilter && ownState.filter || {})).then(result => { + dispatch(new SetResultAction(result)); + }).catch(error => new AddErrorInfoAction(error)); + }; + + const createActions = (dispatch: Dispatch, skipRefresh: boolean = false) => { + return { + onRefresh: () => { + dispatch(reloadAction); + }, + onHandleRequestSort: (orderBy: string) => { + dispatch((dispatch: Dispatch) => { + dispatch(new RequestSortAction(orderBy)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onToggleFilter: () => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + const { showFilter } = selectState(getAppState()); + dispatch(new SetShowFilterAction(!showFilter)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onFilterChanged: (property: string, filterTerm: string) => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + let { filter } = selectState(getAppState()); + filter = { ...filter, [property]: filterTerm }; + dispatch(new SetFilterChangedAction(filter)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onHandleChangePage: (page: number) => { + dispatch((dispatch: Dispatch) => { + dispatch(new SetPageAction(page)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => { + dispatch((dispatch: Dispatch) => { + dispatch(new SetRowsPerPageAction(rowsPerPage || 10)); + (!skipRefresh) && dispatch(reloadAction); + }); + } + // selected: + }; + }; + + const createProperties = (state: IApplicationStoreState) => { + return { + ...selectState(state) + } + } + + return { + reloadAction: reloadAction, + createActions: createActions, + createProperties: createProperties, + actionHandler: externalTableStateActionHandler + } +}
\ No newline at end of file |