From a763f3ec5f07b310d730b4dafc2344a93c30040c Mon Sep 17 00:00:00 2001 From: bskjon Date: Thu, 27 Feb 2025 01:02:27 +0100 Subject: [PATCH] Added missing ui stuff --- .../app/features/table/expandableTable.tsx | 147 ++++++++++++++++++ .../src/app/features/table/sortableTable.tsx | 126 +++++++++++++++ apps/ui/web/src/app/features/table/table.ts | 17 ++ .../web/src/app/store/chained-events-slice.ts | 40 +++++ 4 files changed, 330 insertions(+) create mode 100644 apps/ui/web/src/app/features/table/expandableTable.tsx create mode 100644 apps/ui/web/src/app/features/table/sortableTable.tsx create mode 100644 apps/ui/web/src/app/features/table/table.ts create mode 100644 apps/ui/web/src/app/store/chained-events-slice.ts diff --git a/apps/ui/web/src/app/features/table/expandableTable.tsx b/apps/ui/web/src/app/features/table/expandableTable.tsx new file mode 100644 index 00000000..aa0bc5e8 --- /dev/null +++ b/apps/ui/web/src/app/features/table/expandableTable.tsx @@ -0,0 +1,147 @@ +import { Box, TableContainer, Table, TableHead, TableRow, TableCell, Typography, TableBody, useTheme } from "@mui/material"; +import { useState, useEffect } from "react"; +import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table"; +import IconArrowUp from '@mui/icons-material/ArrowUpward'; +import IconArrowDown from '@mui/icons-material/ArrowDownward'; + +export interface ExpandableItem { + tag: string; + expandElement: JSX.Element | null; +} + +export type ExpandableRender = (item: T) => ExpandableItem | null; + + +export default function ExpandableTable({ items, columns, cellCustomizer: customizer, expandableRender, onRowClickedEvent }: { items: Array, columns: Array, cellCustomizer?: TableCellCustomizer, expandableRender: ExpandableRender, onRowClickedEvent?: TableRowActionEvents }) { + const muiTheme = useTheme(); + + const [order, setOrder] = useState<'asc' | 'desc'>('asc'); + const [orderBy, setOrderBy] = useState(''); + const [selectedRow, setSelectedRow] = useState(null); + + const tableRowSingleClicked = (row: T | null) => { + if (row === selectedRow) { + setSelectedRow(null); + } else { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.click(row); + } + } + } + const tableRowDoubleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.doubleClick(row); + } + } + + const tableRowContextMenu = (e: React.MouseEvent , row: T | null) => { + if (row && onRowClickedEvent && onRowClickedEvent.contextMenu) { + e.preventDefault() + onRowClickedEvent.contextMenu(row, e.pageX, e.pageY) + } + } + + const handleSort = (property: string) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const compareValues = (a: any, b: any, orderBy: string) => { + if (typeof a[orderBy] === 'string') { + return a[orderBy].localeCompare(b[orderBy]); + } else if (typeof a[orderBy] === 'number') { + return a[orderBy] - b[orderBy]; + } + return 0; + }; + + const sortedData = items.slice().sort((a, b) => { + if (order === 'asc') { + return compareValues(a, b, orderBy); + } else { + return compareValues(b, a, orderBy); + } + }); + + useEffect(() => { + handleSort(columns[0].accessor) + }, []) + + + return ( + + + + + + {columns.map((column) => ( + handleSort(column.accessor)} sx={{ cursor: "pointer" }}> + + {orderBy === column.accessor ? + (order === "asc" ? () : ()) : ( + + ) + } + {column.label} + + + ))} + + + + {sortedData?.map((row: T, rowIndex: number) => ( + <> + tableRowSingleClicked(row)} + onDoubleClick={() => tableRowDoubleClicked(row)} + onContextMenu={(e) => { + tableRowContextMenu(e, row); + tableRowSingleClicked(row); + }} + style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }} + > + {columns.map((column) => ( + + {customizer && customizer(column.accessor, row) !== null + ? customizer(column.accessor, row) + : {(row as any)[column.accessor]}} + + ))} + + {(selectedRow == row) ? + ( + + { + expandableRender(row)?.expandElement + } + + ): null + } + + + ))} + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/features/table/sortableTable.tsx b/apps/ui/web/src/app/features/table/sortableTable.tsx new file mode 100644 index 00000000..2b5c50c0 --- /dev/null +++ b/apps/ui/web/src/app/features/table/sortableTable.tsx @@ -0,0 +1,126 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "../../store"; +import IconArrowUp from '@mui/icons-material/ArrowUpward'; +import IconArrowDown from '@mui/icons-material/ArrowDownward'; +import { Table, TableHead, TableRow, TableCell, TableBody, Typography, Box, useTheme, TableContainer } from "@mui/material"; +import { TablePropetyConfig, TableCellCustomizer, TableRowActionEvents } from "./table"; + + +export default function SimpleTable({ items, columns, customizer, onRowClickedEvent }: { items: Array, columns: Array, customizer?: TableCellCustomizer, onRowClickedEvent?: TableRowActionEvents }) { + const muiTheme = useTheme(); + + const [order, setOrder] = useState<'asc' | 'desc'>('asc'); + const [orderBy, setOrderBy] = useState(''); + const [selectedRow, setSelectedRow] = useState(null); + + const tableRowSingleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.click(row); + } + } + const tableRowDoubleClicked = (row: T | null) => { + setSelectedRow(row); + if (row && onRowClickedEvent) { + onRowClickedEvent.doubleClick(row); + } + } + + const tableRowContextMenu = (e: React.MouseEvent , row: T | null) => { + if (row && onRowClickedEvent && onRowClickedEvent.contextMenu) { + e.preventDefault() + onRowClickedEvent.contextMenu(row, e.pageX, e.pageY) + } + } + + const handleSort = (property: string) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const compareValues = (a: any, b: any, orderBy: string) => { + if (typeof a[orderBy] === 'string') { + return a[orderBy].localeCompare(b[orderBy]); + } else if (typeof a[orderBy] === 'number') { + return a[orderBy] - b[orderBy]; + } + return 0; + }; + + const sortedData = items.slice().sort((a, b) => { + if (order === 'asc') { + return compareValues(a, b, orderBy); + } else { + return compareValues(b, a, orderBy); + } + }); + + useEffect(() => { + handleSort(columns[0].accessor) + }, []) + + + return ( + + + + + + {columns.map((column) => ( + handleSort(column.accessor)} sx={{ cursor: "pointer" }}> + + {orderBy === column.accessor ? + (order === "asc" ? () : ()) : ( + + ) + } + {column.label} + + + ))} + + + + {sortedData?.map((row: T, rowIndex: number) => ( + tableRowSingleClicked(row)} + onDoubleClick={() => tableRowDoubleClicked(row)} + onContextMenu={(e) => { + tableRowContextMenu(e, row); + tableRowSingleClicked(row); + }} + style={{ cursor: "pointer", backgroundColor: selectedRow === row ? muiTheme.palette.action.selected : '' }} + > + {columns.map((column) => ( + + {customizer && customizer(column.accessor, row) !== null + ? customizer(column.accessor, row) + : {(row as any)[column.accessor]}} + + ))} + + ))} + +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/ui/web/src/app/features/table/table.ts b/apps/ui/web/src/app/features/table/table.ts new file mode 100644 index 00000000..01ed24d5 --- /dev/null +++ b/apps/ui/web/src/app/features/table/table.ts @@ -0,0 +1,17 @@ + + +export interface TablePropetyConfig { + label: string + accessor: string +} + +export interface TableCellCustomizer { + (accessor: string, data: T): JSX.Element | null +} + +type NullableTableRowActionEvents = TableRowActionEvents | null; +export interface TableRowActionEvents { + click: (row: T) => void; + doubleClick: (row: T) => void; + contextMenu?: (row: T, x: number, y: number) => void; +} \ No newline at end of file diff --git a/apps/ui/web/src/app/store/chained-events-slice.ts b/apps/ui/web/src/app/store/chained-events-slice.ts new file mode 100644 index 00000000..a7833a38 --- /dev/null +++ b/apps/ui/web/src/app/store/chained-events-slice.ts @@ -0,0 +1,40 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit" + +export interface EventGroup { + referenceId: string, + created: number, + fileName: string|null, + events: EventChain[] +} + +export interface EventChain { + eventId: string, + eventName: string, + created: number, + success: boolean, + skipped: boolean, + failure: boolean, + events: Array +} + +export interface EventGroups { + groups: Array +} + +const initialState: EventGroups = { + groups: [] +}; + + +const chainedEventsSlice = createSlice({ + name: "ChainedEvents", + initialState, + reducers: { + set: (state, action: PayloadAction>) => { + state.groups = action.payload; + }, + }, +}) + +export const { set } = chainedEventsSlice.actions; +export default chainedEventsSlice.reducer; \ No newline at end of file