From 3d202a04b99f0e61b6ccf8b7a5610e1a15ca58e7 Mon Sep 17 00:00:00 2001 From: Herbert Eiselt Date: Mon, 11 Feb 2019 14:54:12 +0100 Subject: 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 --- .../framework/src/components/errorDisplay.tsx | 105 +++++ sdnr/wt/odlux/framework/src/components/logo.tsx | 81 ++++ .../src/components/material-table/columnModel.ts | 27 ++ .../src/components/material-table/index.tsx | 423 +++++++++++++++++++++ .../src/components/material-table/tableFilter.tsx | 67 ++++ .../src/components/material-table/tableHead.tsx | 84 ++++ .../src/components/material-table/tableToolbar.tsx | 131 +++++++ .../src/components/material-table/utilities.ts | 207 ++++++++++ .../framework/src/components/material-ui/index.ts | 3 + .../src/components/material-ui/listItemLink.tsx | 50 +++ .../framework/src/components/material-ui/panel.tsx | 56 +++ .../src/components/material-ui/snackDisplay.tsx | 57 +++ .../src/components/material-ui/toggleButton.tsx | 159 ++++++++ .../components/material-ui/toggleButtonGroup.tsx | 19 + .../src/components/material-ui/treeView.tsx | 251 ++++++++++++ .../framework/src/components/navigationMenu.tsx | 59 +++ .../framework/src/components/routing/appFrame.tsx | 37 ++ .../wt/odlux/framework/src/components/titleBar.tsx | 125 ++++++ 18 files changed, 1941 insertions(+) create mode 100644 sdnr/wt/odlux/framework/src/components/errorDisplay.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/logo.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/index.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/utilities.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/index.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/navigationMenu.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/titleBar.tsx (limited to 'sdnr/wt/odlux/framework/src/components') diff --git a/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx b/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx new file mode 100644 index 000000000..b5f8f385d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import Modal from '@material-ui/core/Modal'; +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; + +import { ClearErrorInfoAction, RemoveErrorInfoAction } from '../actions/errorActions'; + +import connect, { Connect } from '../flux/connect'; + +const styles = (theme: Theme) => createStyles({ + modal: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + paper: { + width: theme.spacing.unit * 50, + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[5], + padding: theme.spacing.unit * 4, + }, + card: { + minWidth: 275, + }, + bullet: { + display: 'inline-block', + margin: '0 2px', + transform: 'scale(0.8)', + }, + title: { + marginBottom: 16, + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, +}); + +type ErrorDisplayProps = WithStyles & Connect; + +// function getModalStyle() { +// const top = 50 + rand(); +// const left = 50 + rand(); + +// return { +// top: `${ top }%`, +// left: `${ left }%`, +// transform: `translate(-${ top }%, -${ left }%)`, +// }; +// } + +/** + * Represents a compnent for formaing and displaying errors. + */ +class ErrorDisplayComponent extends React.Component { + render(): JSX.Element { + const { classes, state } = this.props; + const errorInfo = state.framework.applicationState.errors.length && state.framework.applicationState.errors[state.framework.applicationState.errors.length - 1]; + return ( + 0 } + onClose={ () => this.props.dispatch(new ClearErrorInfoAction()) } + > + { errorInfo && +
+ + + + Something went wrong. + + + { errorInfo.error && errorInfo.error.toString() } + + + { errorInfo.message && errorInfo.message .toString() } + + + { errorInfo.info && errorInfo.info.componentStack && errorInfo.info.componentStack.split('\n').map(line => { + return [line,
]; + }) } + { errorInfo.info && errorInfo.info.extra && errorInfo.info.extra.split('\n').map(line => { + return [line,
]; + }) } +
+
+ + + +
+
|| null + } +
+ ); + } +} + +export const ErrorDisplay = withStyles(styles)(connect()(ErrorDisplayComponent)); +export default ErrorDisplay; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/logo.tsx b/sdnr/wt/odlux/framework/src/components/logo.tsx new file mode 100644 index 000000000..95c06a30c --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/logo.tsx @@ -0,0 +1,81 @@ +/****************************************************************************** + * Copyright 2018 highstreet technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; // infra for styling + + +import defaultLogo from '../assets/images/defaultLogo.svg'; + +const styles = (theme: Theme) => createStyles({ + headerLogo: { + backgroundImage: "url(" + (theme.design && theme.design.url || defaultLogo) + ")", + backgroundColor: theme.palette.primary.main, + backgroundRepeat: "no-repeat", + backgroundSize: "auto " + (theme.design && theme.design.logoHeight || 70) + "px", + height: theme.design && theme.design.logoHeight || 70, + width: theme.design ? theme.design.width / theme.design.height * theme.design.logoHeight : 220 + } +}); + +type LogoProps = RouteComponentProps<{ id: string }> & WithStyles; +interface ILogoState { + windowWidth: number +} + +class LogoComponent extends React.Component { + + private hideLogoWhenWindowWidthIsLower: number = 800; + + constructor(props: LogoProps) { + super(props); + this.state = { + windowWidth: 0 + }; + this.updateWindowDimensions = this.updateWindowDimensions.bind(this); + } + + componentDidMount(): void { + this.updateWindowDimensions(); + window.addEventListener('resize', this.updateWindowDimensions); + }; + componentWillUnmount(): void { + window.removeEventListener('resize', this.updateWindowDimensions); + }; + updateWindowDimensions(): void { + this.setState({ windowWidth: window.innerWidth }); + } + + render(): JSX.Element { + let div: JSX.Element =
; + if (this.state.windowWidth >= this.hideLogoWhenWindowWidthIsLower) { + div =
; + } else { + console.info([ + "Logo hidden, because browser window width (", + this.state.windowWidth, + "px) is lower thershold (", + this.hideLogoWhenWindowWidthIsLower, + "px)."].join('')); + } + return div; + } +} + +export const Logo = withStyles(styles)(withRouter(LogoComponent)); +export default Logo; 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 = { + rowData: TData +} + +export type ColumnModel = { + title?: string; + disablePadding?: boolean; + width?: string | number; + disableSorting?: boolean; + disableFilter?: boolean; +} & ({ + property: string; + type: ColumnType.custom; + customControl: React.ComponentType>; +} | { + 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 = { page: number, rowCount: number, rows: TData[] }; + +export type DataCallback = (page?: number, rowsPerPage?: number, orderBy?: string | null, order?: 'asc' | 'desc' | null, filter?: { [property: string]: string }) =>resultType | Promise>; + +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 = { + 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 }; + +type MaterialTableComponentBaseProps = WithStyles & { + columns: ColumnModel[]; + idProperty: keyof TData | ((data: TData) => React.Key ); + title?: string; + enableSelection?: boolean; + disableSorting?: boolean; + disableFilter?: boolean; + customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void }[]; + onHandleClick?(event: React.MouseEvent, rowData: TData): void; +}; + +type MaterialTableComponentPropsWithRows = MaterialTableComponentBaseProps & { rows: TData[]; asynchronus?: boolean; }; +type MaterialTableComponentPropsWithRequestData = MaterialTableComponentBaseProps & { onRequestData: DataCallback; tableApi?: TableApi; }; +type MaterialTableComponentPropsWithExternalState = MaterialTableComponentBaseProps & MaterialTableComponentState & { + onToggleFilter: () => void; + onFilterChanged: (property: string, filterTerm: string) => void; + onHandleChangePage: (page: number) => void; + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void; + onHandleRequestSort: (property: string) => void; +}; + +type MaterialTableComponentProps = + MaterialTableComponentPropsWithRows | + MaterialTableComponentPropsWithRequestData | + MaterialTableComponentPropsWithExternalState; + +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 extends React.Component { + + 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 ( + + +
+ + + + { showFilter && || null } + { rows // may need ordering here + .map((entry: TData & { [key: string]: any }) => { + const entryId = getId(entry); + const isSelected = this.isSelected(entryId); + return ( + this.handleClick(event, entry, entryId) } + role="checkbox" + aria-checked={ isSelected } + tabIndex={ -1 } + key={ entryId } + selected={ isSelected } + > + { this.props.enableSelection + ? + + + : null + } + { + this.props.columns.map( + col => { + const style = col.width ? { width: col.width } : {}; + return ( + + { col.type === ColumnType.custom && col.customControl + ? + : entry[col.property] + } + + ); + } + ) + } + + ); + }) } + { emptyRows > 0 && ( + + + + ) } + +
+
+ +
+ ); + } + + 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 | null, page: number) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleChangePage(page); + return; + } + this.setState({ + page + }, this.update); + }; + + private onHandleChangeRowsPerPage = (event: React.ChangeEvent) => { + 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, 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 = new () => React.Component, '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 { + onFilterChanged: (property: string, filterTerm: string) => void; + filter: { [property: string]: string }; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableFilterComponent extends React.Component { + createFilterHandler = (property: string) => (event: React.ChangeEvent) => { + this.props.onFilterChanged && this.props.onFilterChanged(property, event.target.value); + }; + + render() { + const { columns, filter, classes } = this.props; + return ( + + { this.props.enableSelection + ? + + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + + { col.disableFilter || (col.type === ColumnType.custom) ? null : } + + ); + }, this) } + + ); + } +} + +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 { + createSortHandler = (property: string) => (event: React.SyntheticEvent) => { + this.props.onRequestSort(event, property); + }; + + render() { + const { onSelectAllClick, order, orderBy, numSelected, rowCount, columns } = this.props; + + return ( + + + { this.props.enableSelection + ? + 0 && numSelected < rowCount || undefined } + checked={ numSelected === rowCount } + onChange={ onSelectAllClick } + /> + + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + + { col.disableSorting || (col.type === ColumnType.custom) + ? + { col.title || col.property } + + : + + { col.title || col.property } + + } + + ); + }, this) } + + + ); + } +} + +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 { + numSelected: number | null; + title?: string; + customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void }[]; + onToggleFilter: () => void; + onExportToCsv: () => void; +} + +class TableToolbarComponent extends React.Component { + constructor(props: ITableToolbarComponentProps) { + super(props); + + this.state = { + anchorEl: null + }; + } + + private handleMenu = (event: React.MouseEvent) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private handleClose = () => { + this.setState({ anchorEl: null }); + }; + render() { + const { numSelected, classes } = this.props; + const open = !!this.state.anchorEl; + + return ( + 0 ? classes.highlight : '' } ` } > +
+ { numSelected && numSelected > 0 ? ( + + { numSelected } selected + + ) : ( + + { this.props.title || null } + + ) } +
+
+
+ { this.props.customActionButtons + ? this.props.customActionButtons.map((action, ind) =>( + + action.onClick() }> + + + + )) + : null } + { numSelected && numSelected > 0 ? ( + + + + + + ) : ( + + { this.props.onToggleFilter && this.props.onToggleFilter() } }> + + + + ) } + + + + + + + Export as CSV + +
+ + ); + } +}; + +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 { + 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(callback: DataCallback, selectState: (appState: IApplicationStoreState) => IExternalTableState) { + + //#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 = { + order: 'asc', + orderBy: null, + selected: null, + rows: [], + rowCount: 0, + page: 0, + rowsPerPage: 10, + loading: false, + showFilter: false, + filter: {} + }; + + const externalTableStateActionHandler: IActionHandler> = (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 diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/index.ts b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts new file mode 100644 index 000000000..890312ce2 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts @@ -0,0 +1,3 @@ +export { ListItemLink } from './listItemLink'; +export { Panel } from './panel'; +export { ToggleButton, ToggleButtonClassKey } from './toggleButton'; diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx new file mode 100644 index 000000000..6ace59534 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { NavLink, Link, Route } from 'react-router-dom'; + +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; + +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +const styles = (theme: Theme) => createStyles({ + active: { + backgroundColor: theme.palette.action.selected + } +}); + +export interface IListItemLinkProps extends WithStyles { + icon: JSX.Element | null; + primary: string | React.ComponentType; + secondary?: React.ComponentType; + to: string; + exact?: boolean; +} + +export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => { + const { icon, primary: Primary, secondary: Secondary, classes, to, exact = false } = props; + const renderLink = (itemProps: any): JSX.Element => (); + + return ( + <> + + { icon + ? { icon } + : null + } + { typeof Primary === 'string' + ? + : + } + + { Secondary + ? + : null + } + + ); + } +); + +export default ListItemLink; + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx new file mode 100644 index 000000000..0b64666c0 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; + +import { withStyles, Theme, WithStyles, createStyles } from '@material-ui/core/styles'; + +import { ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Typography, ExpansionPanelActions } from '@material-ui/core'; + +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + accordion: { + // background: theme.palette.secondary.dark, + // color: theme.palette.primary.contrastText + }, + detail: { + // background: theme.palette.background.paper, + // color: theme.palette.text.primary, + position: "relative", + display: 'flex', + flexDirection: 'column' + }, + text: { + // color: theme.palette.common.white, + // fontSize: "1rem" + }, +}); + +type PanalProps = WithStyles & { + activePanel: string | null, + panelId: string, + title: string, + customActionButtons?: JSX.Element[]; + onToggle: (panelId: string | null) => void; +} + +const PanelComponent: React.SFC = (props) => { + const { classes, activePanel, onToggle } = props; + return ( + onToggle(props.panelId) } > + }> + { props.title } + + + { props.children } + + { props.customActionButtons + ? + { props.customActionButtons } + + : null } + + ); +}; + +export const Panel = withStyles(styles)(PanelComponent); +export default Panel; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx new file mode 100644 index 000000000..c02bf93e9 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; + +import { IApplicationStoreState } from '../../store/applicationStore'; +import { Connect, connect, IDispatcher } from '../../flux/connect'; +import { RemoveSnackbarNotification } from '../../actions/snackbarActions'; + +import { InjectedNotistackProps, withSnackbar } from 'notistack'; + +const mapProps = (state: IApplicationStoreState) => ({ + notifications: state.framework.applicationState.snackBars +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + removeSnackbar: (key: number) => { + dispatcher.dispatch(new RemoveSnackbarNotification(key)); + } +}); + +type DisplaySnackbarsComponentProps = Connect & InjectedNotistackProps; + +class DisplaySnackbarsComponent extends React.Component { + private displayed: number[] = []; + + private storeDisplayed = (id: number) => { + this.displayed = [...this.displayed, id]; + }; + + public shouldComponentUpdate({ notifications: newSnacks = [] }: DisplaySnackbarsComponentProps) { + + const { notifications: currentSnacks } = this.props; + let notExists = false; + for (let i = 0; i < newSnacks.length; i++) { + if (notExists) continue; + notExists = notExists || !currentSnacks.filter(({ key }) => newSnacks[i].key === key).length; + } + return notExists; + } + + componentDidUpdate() { + const { notifications = [] } = this.props; + + notifications.forEach(notification => { + if (this.displayed.includes(notification.key)) return; + const options = notification.options || {}; + this.props.enqueueSnackbar(notification.message, options); + this.storeDisplayed(notification.key); + this.props.removeSnackbar(notification.key); + }); + } + + render() { + return null; + } +} + +const DisplayStackbars = withSnackbar(connect(mapProps, mapDispatch)(DisplaySnackbarsComponent)); +export default DisplayStackbars; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx new file mode 100644 index 000000000..522ff12c8 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx @@ -0,0 +1,159 @@ + +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import ButtonBase from '@material-ui/core/ButtonBase'; + + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + ...theme.typography.button, + height: 32, + minWidth: 48, + margin: 0, + padding: `${theme.spacing.unit - 4}px ${theme.spacing.unit * 1.5}px`, + borderRadius: 2, + willChange: 'opacity', + color: fade(theme.palette.action.active, 0.38), + '&:hover': { + textDecoration: 'none', + // Reset on mouse devices + backgroundColor: fade(theme.palette.text.primary, 0.12), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + '&$disabled': { + backgroundColor: 'transparent', + }, + }, + '&:not(:first-child)': { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }, + '&:not(:last-child)': { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + }, + /* Styles applied to the root element if `disabled={true}`. */ + disabled: { + color: fade(theme.palette.action.disabled, 0.12), + }, + /* Styles applied to the root element if `selected={true}`. */ + selected: { + color: theme.palette.action.active, + '&:after': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + borderRadius: 'inherit', + width: '100%', + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.38, + }, + '& + &:before': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + width: 1, + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.12, + }, + }, + /* Styles applied to the `label` wrapper element. */ + label: { + width: '100%', + display: 'inherit', + alignItems: 'inherit', + justifyContent: 'inherit', + }, +}); + +export type ToggleButtonClassKey = 'disabled' | 'root' | 'label' | 'selected'; + +interface IToggleButtonProps extends WithStyles { + className?: string; + component?: React.ReactType; + disabled?: boolean; + disableFocusRipple?: boolean; + disableRipple?: boolean; + selected?: boolean; + type?: string; + value?: any; + onClick?: (event: React.FormEvent, value?: any) => void; + onChange?: (event: React.FormEvent, value?: any) => void; +} + +class ToggleButtonComponent extends React.Component { + handleChange = (event: React.FormEvent) => { + const { onChange, onClick, value } = this.props; + + if (onClick) { + onClick(event, value); + if (event.isDefaultPrevented()) { + return; + } + } + + if (onChange) { + onChange(event, value); + } + }; + + render() { + const { + children, + className: classNameProp, + classes, + disableFocusRipple, + disabled, + selected, + ...other + } = this.props; + + const className = classNames( + classes.root, + { + [classes.disabled]: disabled, + [classes.selected]: selected, + }, + classNameProp, + ); + + return ( + + {children} + + ); + } + public static defaultProps = { + disabled: false, + disableFocusRipple: false, + disableRipple: false, + }; + + public static muiName = 'ToggleButton'; +} + +export const ToggleButton = withStyles(styles, { name: 'MuiToggleButton' })(ToggleButtonComponent); +export default ToggleButton; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx new file mode 100644 index 000000000..8ab7c2b91 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + transition: theme.transitions.create('background,box-shadow'), + background: 'transparent', + borderRadius: 2, + overflow: 'hidden', + }, + /* Styles applied to the root element if `selected={true}` or `selected="auto" and `value` set. */ + selected: { + background: theme.palette.background.paper, + boxShadow: theme.shadows[2], + }, +}); + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx new file mode 100644 index 000000000..8bcdc8bc6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx @@ -0,0 +1,251 @@ +import * as React from 'react'; + +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core'; + +import FileIcon from '@material-ui/icons/InsertDriveFile'; +import CloseIcon from '@material-ui/icons/ExpandLess'; +import OpenIcon from '@material-ui/icons/ExpandMore'; +import FolderIcon from '@material-ui/icons/Folder'; + +export interface ITreeItem { + disabled?: boolean; + icon?: React.ComponentType; +} + +type TreeViewComponentState = { + /** All indices of all expanded Items */ + expandedItems: TData[]; + /** The index of the active iten or undefined if no item is active. */ + activeItem: undefined | TData; + /** The search term or undefined if search is corrently not active. */ + searchTerm: undefined | string; +} + +type TreeViewComponentBaseProps = WithTheme & { + items: TData[]; + contentProperty: keyof Omit; + childrenProperty: keyof Omit; + useFolderIcons?: boolean; + enableSearchBar?: boolean; + autoExpandFolder?: boolean; + style?: React.CSSProperties; + itemHeight?: number; + depthOffset?: number; +} + +type TreeViewComponentWithInternalStateProps = TreeViewComponentBaseProps & { + onItemClick?: (item: TData) => void; + onFolderClick?: (item: TData) => void; +} + +type TreeViewComponentWithExternalStateProps = TreeViewComponentBaseProps & TreeViewComponentState & { + onSearch: (searchTerm: string) => void; + onItemClick: (item: TData) => void; + onFolderClick: (item: TData) => void; +} + +type TreeViewComponentProps = + TreeViewComponentWithInternalStateProps | + TreeViewComponentWithExternalStateProps; + +function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps { + const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps) + return ( + propsWithExternalState.onSearch instanceof Function || + propsWithExternalState.expandedItems !== undefined || + propsWithExternalState.activeItem !== undefined || + propsWithExternalState.searchTerm !== undefined + ); +} + +class TreeViewComponent extends React.Component, TreeViewComponentState> { + + /** + * Initializes a new instance. + */ + constructor(props: TreeViewComponentProps) { + super(props); + + this.state = { + expandedItems: [], + activeItem: undefined, + searchTerm: undefined + }; + } + + render(): JSX.Element { + this.itemIndex = 0; + const { searchTerm } = this.state; + const { children, items, enableSearchBar } = this.props; + const styles = { + root: { + padding: 0, + paddingBottom: 8, + paddingTop: children ? 0 : 8, + ...this.props.style + }, + search: { + padding: `0px ${ this.props.theme.spacing.unit }px` + } + }; + return ( +
+ { children } + { enableSearchBar && || null } + + { this.renderItems(items, searchTerm && searchTerm.toLowerCase()) } + +
+ ); + } + + private itemIndex: number = 0; + private renderItems = (items: TData[], searchTerm: string | undefined, depth: number = 1) => { + return items.reduce((acc, item) => { + + const children = this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); + const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1); + + const expanded = searchTerm + ? children && childrenJsx.length > 0 + : !children + ? false + : this.state.expandedItems.indexOf(item) > -1; + const isFolder = children !== undefined; + + const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded); + itemJsx && acc.push(itemJsx); + + if (isFolder && expanded) { + acc.push(...childrenJsx); + } + return acc; + + }, [] as JSX.Element[]); + } + private renderItem = (item: TData, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => { + const styles = { + item: { + paddingLeft: (((this.props.depthOffset || 0) + depth) * this.props.theme.spacing.unit * 3), + backgroundColor: this.state.activeItem === item ? this.props.theme.palette.action.selected : undefined, + height: this.props.itemHeight || undefined, + cursor: item.disabled ? 'not-allowed' : 'pointer', + color: item.disabled ? this.props.theme.palette.text.disabled : this.props.theme.palette.text.primary, + overflow: 'hidden', + transform: 'translateZ(0)', + } + }; + + const text = (item as any)[this.props.contentProperty] as string || ''; // need to keep track of search + const matchIndex = searchTerm ? text.toLowerCase().indexOf(searchTerm) : -1; + const searchTermLength = searchTerm && searchTerm.length || 0; + + const handleClickCreator = (isIcon: boolean) => (event: React.SyntheticEvent) => { + if (item.disabled) return; + event.preventDefault(); + event.stopPropagation(); + if (isFolder && (this.props.autoExpandFolder || isIcon)) { + this.props.onFolderClick ? this.props.onFolderClick(item) : this.onFolderClick(item); + } else { + this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item); + } + }; + + return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm) + ? ( + + + { // display the left icon + (this.props.useFolderIcons && { isFolder ? : }) || + (item.icon && ()) } + + + { // highlight search result + matchIndex > -1 + ? ( + { text.substring(0, matchIndex) } + + { text.substring(matchIndex, matchIndex + searchTermLength) } + + { text.substring(matchIndex + searchTermLength) } + ) + : () + } + + { // display the right icon, depending on the state + !isFolder ? null : expanded ? () : () } + + ) + : null + ); + } + + private onFolderClick = (item: TData) => { + // toggle items with children + if (this.state.searchTerm) return; + const indexOfItemToToggle = this.state.expandedItems.indexOf(item); + if (indexOfItemToToggle === -1) { + this.setState({ + expandedItems: [...this.state.expandedItems, item], + }); + } else { + this.setState({ + expandedItems: [ + ...this.state.expandedItems.slice(0, indexOfItemToToggle), + ...this.state.expandedItems.slice(indexOfItemToToggle + 1), + ] + }); + } + }; + + private onItemClick = (item: TData) => { + // activate items without children + this.setState({ + activeItem: item, + }); + }; + + private onChangeSearchText = (event: React.ChangeEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (isTreeViewComponentWithExternalStateProps(this.props)) { + this.props.onSearch(event.target.value) + } else { + this.setState({ + searchTerm: event.target.value + }); + } + }; + + static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState { + if (isTreeViewComponentWithExternalStateProps(props)) { + return { + ...state, + expandedItems: props.expandedItems || [], + activeItem: props.activeItem, + searchTerm: props.searchTerm + }; + } + return state; + } + + public static defaultProps = { + useFolderIcons: false, + enableSearchBar: false, + autoExpandFolder: false, + depthOffset: 0 + } +} + +export type TreeViewCtorType = new () => React.Component, 'theme'>>; + +export const TreeView = withTheme()(TreeViewComponent); +export default TreeView; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx new file mode 100644 index 000000000..f6df244a0 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import { faHome, faAddressBook } from '@fortawesome/free-solid-svg-icons'; + +import Drawer from '@material-ui/core/Drawer'; +import List from '@material-ui/core/List'; + +import Divider from '@material-ui/core/Divider'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import ListItemLink from '../components/material-ui/listItemLink'; + +import connect, { Connect } from '../flux/connect'; + +const drawerWidth = 240; + +const styles = (theme: Theme) => createStyles({ + drawerPaper: { + position: 'relative', + width: drawerWidth, + }, + toolbar: theme.mixins.toolbar +}); + +export const NavigationMenu = withStyles(styles)(connect()(({ classes, state }: WithStyles & Connect) => { + return ( + +
+ { /* https://fiffty.github.io/react-treeview-mui/ */} + + { process.env.NODE_ENV === "development" ? } /> : null } + + { + state.framework.applicationRegistraion && Object.keys(state.framework.applicationRegistraion).map(key => { + const reg = state.framework.applicationRegistraion[key]; + return reg && ( + || null} /> + ) || null; + }) || null + } + + { process.env.NODE_ENV === "development" ? } /> : null } + + ) +})); + +export default NavigationMenu; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx b/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx new file mode 100644 index 000000000..55b249246 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; + +import connect, { Connect } from '../../flux/connect'; + +import { SetTitleAction } from '../../actions/titleActions'; +import { AddErrorInfoAction } from '../../actions/errorActions'; + +import { IconType } from '../../models/iconDefinition'; + +export interface IAppFrameProps { + title: string; + icon?: IconType; +} + +/** + * Represents a component to wich will embed each single app providing the + * functionality to update the title and implement an exeprion border. + */ +export class AppFrame extends React.Component { + + public render(): JSX.Element { + return ( +
+ { this.props.children } +
+ ) + } + + public componentDidMount() { + this.props.dispatch(new SetTitleAction(this.props.title, this.props.icon)); + } + public componentDidCatch(error: Error | null, info: object) { + this.props.dispatch(new AddErrorInfoAction({ error, info })); + } +} + +export default connect()(AppFrame); \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/titleBar.tsx b/sdnr/wt/odlux/framework/src/components/titleBar.tsx new file mode 100644 index 000000000..d4d17d22e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/titleBar.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { UpdateAuthentication } from '../actions/authentication'; + +import connect, { Connect, IDispatcher } from '../flux/connect'; +import Logo from './logo'; + +const styles = (theme: Theme) => createStyles({ + appBar: { + zIndex: theme.zIndex.drawer + 1, + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + icon: { + marginRight: 8, + marginLeft: 24, + } +}); + +const mapDispatch = (dispatcher: IDispatcher) => { + return { + logout: () => { dispatcher.dispatch(new UpdateAuthentication(null)); } + } +}; + +type TitleBarProps = RouteComponentProps<{}> & WithStyles & Connect + +class TitleBarComponent extends React.Component { + + constructor(props: TitleBarProps) { + super(props); + + this.state = { + anchorEl: null + } + + } + render(): JSX.Element { + const { classes, state, history, location } = this.props; + const open = !!this.state.anchorEl; + + return ( + + + + + + + + { state.framework.applicationState.icon + ? () + : null } + { state.framework.applicationState.title } + + { state.framework.authenticationState.user + ? (
+ + + Profile + { + this.props.logout(); + this.closeMenu(); + } }>Logout + +
) + : () } +
+
+ ); + }; + + + private openMenu = (event: React.MouseEvent) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private closeMenu = () => { + this.setState({ anchorEl: null }); + }; +} + +//todo: ggf. https://github.com/acdlite/recompose verwenden zur Vereinfachung + +export const TitleBar = withStyles(styles)(withRouter(connect(undefined, mapDispatch)(TitleBarComponent))); +export default TitleBar; \ No newline at end of file -- cgit 1.2.3-korg