From 57f4ba3de83a46dce7af46ba8524435be1efa54d Mon Sep 17 00:00:00 2001 From: "Andreas Goelzer (NuoDB)" <139239314+agoelzer@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:28:31 -0500 Subject: [PATCH] make table row popup menu stick to the right of the viewport (#39) --- .../java/com/nuodb/selenium/TestRoutines.java | 4 +- ui/public/theme/base.css | 98 +++++++++ ui/src/App.tsx | 2 + ui/src/components/controls/Menu.tsx | 200 ++++++++++++------ ui/src/components/controls/Table.tsx | 55 +---- ui/src/components/pages/parts/Banner.tsx | 1 + ui/src/components/pages/parts/Table.tsx | 16 +- .../pages/parts/TableSettingsColumns.tsx | 5 +- ui/src/utils/types.ts | 2 +- 9 files changed, 262 insertions(+), 121 deletions(-) diff --git a/selenium-tests/src/test/java/com/nuodb/selenium/TestRoutines.java b/selenium-tests/src/test/java/com/nuodb/selenium/TestRoutines.java index b69c1e0..b44e608 100644 --- a/selenium-tests/src/test/java/com/nuodb/selenium/TestRoutines.java +++ b/selenium-tests/src/test/java/com/nuodb/selenium/TestRoutines.java @@ -132,8 +132,8 @@ public void clickPopupMenu(WebElement element, String dataTestId) { List menuToggles = element.findElements(By.xpath("div[@data-testid='menu-toggle']")); assertEquals(1, menuToggles.size()); menuToggles.get(0).click(); - WebElement parent = menuToggles.get(0).findElement(By.xpath("..")); - List menuItems = parent.findElements(By.xpath(".//div[@data-testid='" + dataTestId + "']")); + WebElement menuPopup = getElement("menu-popup"); + List menuItems = menuPopup.findElements(By.xpath(".//div[@data-testid='" + dataTestId + "']")); assertEquals(1, menuItems.size()); menuItems.get(0).click(); } diff --git a/ui/public/theme/base.css b/ui/public/theme/base.css index caccabf..778a607 100644 --- a/ui/public/theme/base.css +++ b/ui/public/theme/base.css @@ -40,6 +40,9 @@ } .NuoMenuPopupItem { + display: flex; + flex-direction: row; + justify-content: space-between; margin: 0px; padding: 5px 10px; font-family: Roboto, Helvetica, Arial, sans-serif; @@ -52,6 +55,11 @@ white-space: nowrap; } +.NuoMenuPopupItem>svg { + display: flex; + flex: 0 0 auto; + opacity: 0.5; +} .NuoMenuPopupItem:hover { background-color: lightgray; } @@ -68,4 +76,94 @@ .NuoTableNoData { font-size: 1.25em; padding: 25px; +} +.NuoTableContainer { + box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); + background-color: #fff; + color: rgba(0, 0, 0, 0.87); + -webkit-transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + border-radius: 4px; + box-shadow: var(--Paper-shadow); + background-image: var(--Paper-overlay); + overflow: hidden; + width: 100%; + overflow-x: auto; +} + +.NuoTableTable { + display: table; + width: 100%; + border-collapse: collapse; + border-spacing: 0; + text-indent: initial; + unicode-bidi: isolate; + border-color: gray; +} + +.NuoTableThead { + vertical-align: middle; + unicode-bidi: isolate; + border-color: inherit; +} + +.NuoTableTr { + color: inherit; + display: table-row; + vertical-align: middle; + outline: 0; + unicode-bidi: isolate; + border-color: inherit; +} + +.NuoTableSettingsItem { + display: flex; + flex-direction: row; + flex: 1 1 auto; +} + +.NuoTableSettingsItem>label { + display: flex; + flex: 1 1 auto; +} + +.NuoTableTh, +.NuoTableTd { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 0.875rem; + letter-spacing: 0.0108em; + display: table-cell; + padding: 16px; + vertical-align: inherit; + border-bottom: 1px solid rgba(224, 224, 224, 1); + color: rgba(0, 0, 0, 0.87); + unicode-bidi: isolate; + text-align: left; +} + +.NuoTableTh { + line-height: 1.5rem; + font-weight: 500; +} + +.NuoTableTd { + font-weight: 400; + line-height: 1.43; +} + +.NuoTableTbody { + display: table-row-group; + vertical-align: middle; + unicode-bidi: isolate; + border-color: inherit; + border-collapse: collapse; + border-spacing: 0; +} + +.NuoTableMenuCell { + position: sticky; + right: 0; + text-align: right; + background-color: rgba(255, 255, 255, 0.8); + ; } \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7950a95..42d65b8 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -19,6 +19,7 @@ import GlobalErrorBoundary from "./components/GlobalErrorBoundary"; import Auth from "./utils/auth"; import Settings from './components/pages/Settings'; import Customizations from './utils/Customizations'; +import { PopupMenu } from './components/controls/Menu'; /** * React Root Application. Sets up dialogs, BrowserRouter and Schema from Control Plane @@ -32,6 +33,7 @@ export default function App() { + {isLoggedIn diff --git a/ui/src/components/controls/Menu.tsx b/ui/src/components/controls/Menu.tsx index cb65ad5..ad3e299 100644 --- a/ui/src/components/controls/Menu.tsx +++ b/ui/src/components/controls/Menu.tsx @@ -1,19 +1,78 @@ // (C) Copyright 2024 Dassault Systemes SE. All Rights Reserved. -import { useState } from 'react'; +import { Component, useEffect } from 'react'; import Button from './Button'; import MoreVertIcon from '@mui/icons-material/MoreVert'; +import DragHandleIcon from '@mui/icons-material/DragHandle'; import { MenuItemProps, MenuProps } from '../../utils/types'; export default function Menu(props: MenuProps): JSX.Element { - const { align, popup, items, setItems, className, draggable } = props; - const [anchor, setAnchor] = useState(null); - const [dndSelected, setDndSelected] = useState(); + const { popupId, items, className } = props; - function dndIsBefore(el1: any, el2: any) { - let cur + useEffect(() => { + PopupMenu.updateMenu(props) + }, [props]); + + function listMenu(items: MenuItemProps[]) { + return items.map((item, index) => ); + } + + let { children } = props; + if (popupId && !children) { + children = + } + + if (children) { + return <> +
{ + PopupMenu.showMenu(props, event.currentTarget) + }}> + <>{children} +
+ ; + } + else { + return
{listMenu(items)}
; + } +} + +let s_popupInstance: PopupMenu | null = null; + +type AlignType = "right" | "left"; + +interface PopupState extends MenuProps { + dndSelected: any; + anchor: Element | null; +} + +export class PopupMenu extends Component<{}, PopupState> { + state = { + popupId: "", + items: [], + setItems: undefined, + anchor: null, + align: "right" as AlignType, + draggable: false, + + dndSelected: undefined + } + + componentDidMount() { + if (!s_popupInstance) { + s_popupInstance = this; + } + } + + dndIsBefore = (el1: any, el2: any) => { if (el2.parentNode === el1.parentNode) { + let cur for (cur = el1.previousSibling; cur; cur = cur.previousSibling) { if (cur === el2) return true } @@ -22,7 +81,7 @@ export default function Menu(props: MenuProps): JSX.Element { } /* find given element target (or parent) which is draggable */ - function dndGetDraggableTarget(target: any) { + dndGetDraggableTarget = (target: any) => { while (target.getAttribute("draggable") !== "true") { if (!target.parentElement) { return undefined; @@ -32,97 +91,114 @@ export default function Menu(props: MenuProps): JSX.Element { return target; } - function dndOver(e: React.DragEvent) { + dndOver = (e: React.DragEvent) => { e.preventDefault(); - const draggableTarget = dndGetDraggableTarget(e.target); + const draggableTarget = this.dndGetDraggableTarget(e.target); if (!draggableTarget || !draggableTarget.parentNode) { return; } - if (dndIsBefore(dndSelected, draggableTarget)) { - draggableTarget.parentNode.insertBefore(dndSelected, draggableTarget) + if (this.dndIsBefore(this.state.dndSelected, draggableTarget)) { + draggableTarget.parentNode.insertBefore(this.state.dndSelected, draggableTarget) } else { - draggableTarget.parentNode.insertBefore(dndSelected, draggableTarget.nextSibling) + draggableTarget.parentNode.insertBefore(this.state.dndSelected, draggableTarget.nextSibling) } } - function dndDrop(e: React.DragEvent) { - setDndSelected(undefined); - const target = dndGetDraggableTarget(e.target); + dndDrop = (e: React.DragEvent) => { + this.setState({ dndSelected: undefined }); + const target = this.dndGetDraggableTarget(e.target); if (!target?.parentNode?.children) { return; } let newItems: MenuItemProps[] = []; Array.from(target.parentNode.children).forEach((child: any) => { - const newItem = items.find(item => item.id === child.getAttribute("id")); + const newItem = this.state.items.find((item: MenuItemProps) => item.id === child.getAttribute("id")); newItem && newItems.push(newItem); }) - if (setItems) { + if (this.state.setItems) { + const setItems: ((items: MenuItemProps[]) => void) = this.state.setItems; setItems(newItems); } } - function dndStart(e: any) { - const draggableTarget = dndGetDraggableTarget(e.target); + dndStart = (e: any) => { + const draggableTarget = this.dndGetDraggableTarget(e.target); if (!draggableTarget) { return; } e.dataTransfer.effectAllowed = 'move' e.dataTransfer.setData('text', draggableTarget.getAttribute("id")) - setDndSelected(e.target); + this.setState({ dndSelected: e.target }); } - function popupMenu(items: MenuItemProps[]) { - const rect = anchor?.getBoundingClientRect(); - const x = align === "right" ? rect?.right : rect?.left; + static showMenu(menu: MenuProps, anchor: Element): void { + s_popupInstance?.setState({ + "data-testid": undefined, + align: undefined, + popupId: undefined, + draggable: undefined, + children: undefined, + setItems: undefined, + className: undefined, + ...menu, + anchor + }); + } + + static updateMenu(menu: MenuProps): void { + if (s_popupInstance) { + if (s_popupInstance.state.popupId === menu.popupId) { + s_popupInstance.setState(menu); + } + } + } + + render() { + if (!this.state.anchor) { + return null; + } + + const anchor: Element = this.state.anchor; + const rect = anchor.getBoundingClientRect(); + const x = this.state.align === "right" ? rect?.right : rect?.left; return
setAnchor(null)}> -
-
- {items.map(item =>
this.setState({ anchor: null })}> +
+
+ {this.state.items.map((item: MenuItemProps) =>
{ e.stopPropagation(); setAnchor(null); item.onClick && item.onClick(); }}> + onClick={(e) => { + e.stopPropagation(); + if (item.onClick) { + this.setState({ anchor: null }); + item.onClick(); + + } + }}> {item.label} + {this.state.draggable === true && }
)}
; } - - function listMenu(items: MenuItemProps[]) { - return items.map((item, index) => ); - } - - let { children } = props; - if (popup && !children) { - children = - } - - if (children) { - return <> -
{ - setAnchor(event.currentTarget) - }}> - <>{children} -
- {popupMenu(items)} - ; - } - else { - return
{listMenu(items)}
; - } } \ No newline at end of file diff --git a/ui/src/components/controls/Table.tsx b/ui/src/components/controls/Table.tsx index 77aae76..a57e74d 100644 --- a/ui/src/components/controls/Table.tsx +++ b/ui/src/components/controls/Table.tsx @@ -1,6 +1,3 @@ -import { Card, Table as MuiTable, TableHead as MuiTableHead, TableRow as MuiTableRow, TableCell as MuiTableCell, TableBody as MuiTableBody } from '@mui/material'; -import TableContainer from '@mui/material/TableContainer'; -import { isMaterial } from '../../utils/Customizations'; import { ReactNode } from "react"; type TableProps = { @@ -11,62 +8,30 @@ type TableProps = { type ChildProps = { children?: ReactNode, + className?: string, "data-testid"?: string, } export function Table(props: TableProps): JSX.Element { - if (isMaterial()) { - return - - {props.children} - - - } - else { - return {props.children}
- } + return
{props.children}
} export function TableHead(props: ChildProps): JSX.Element { - if (isMaterial()) { - return - {props.children} - - } - else { - return {props.children} - } + return {props.children} } export function TableRow(props: ChildProps): JSX.Element { - if (isMaterial()) { - return - {props.children} - - } - else { - return {props.children} - } + return {props.children} } export function TableCell(props: ChildProps): JSX.Element { - if (isMaterial()) { - return - {props.children} - - } - else { - return {props.children} - } + return {props.children} +} + +export function TableTh(props: ChildProps): JSX.Element { + return {props.children} } export function TableBody(props: ChildProps): JSX.Element { - if (isMaterial()) { - return - {props.children} - - } - else { - return {props.children} - } + return {props.children} } diff --git a/ui/src/components/pages/parts/Banner.tsx b/ui/src/components/pages/parts/Banner.tsx index fae697c..81b3163 100644 --- a/ui/src/components/pages/parts/Banner.tsx +++ b/ui/src/components/pages/parts/Banner.tsx @@ -45,6 +45,7 @@ function ResponsiveAppBar(resources: string[], t: any) { { return { "data-testid": "menu-label-" + resource, diff --git a/ui/src/components/pages/parts/Table.tsx b/ui/src/components/pages/parts/Table.tsx index 33454cd..7d0a129 100644 --- a/ui/src/components/pages/parts/Table.tsx +++ b/ui/src/components/pages/parts/Table.tsx @@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom'; import { withTranslation } from "react-i18next"; -import { TableBody, TableCell, Table as TableCustom, TableHead, TableRow } from '../../controls/Table'; +import { TableBody, TableTh, TableCell, Table as TableCustom, TableHead, TableRow } from '../../controls/Table'; import { getResourceByPath, getCreatePath, getChild, replaceVariables } from "../../../utils/schema"; import FieldFactory from "../../fields/FieldFactory"; import RestSpinner from "./RestSpinner"; @@ -211,8 +211,8 @@ function Table(props: TempAny) { }) } - return - + return + ; } @@ -259,12 +259,12 @@ function Table(props: TempAny) { - {visibleColumns.map((column, index) => + {visibleColumns.map((column, index) => {tableLabels[column.id]} - )} - + )} + {data.length > 0 && } - + @@ -274,7 +274,7 @@ function Table(props: TempAny) { {renderMenuCell(row)} ))} - {data.length === 0 &&
{t("text.noData")}
} + {data.length === 0 &&
{t("text.noData")}
}
); diff --git a/ui/src/components/pages/parts/TableSettingsColumns.tsx b/ui/src/components/pages/parts/TableSettingsColumns.tsx index 67bdc5b..fa0075d 100644 --- a/ui/src/components/pages/parts/TableSettingsColumns.tsx +++ b/ui/src/components/pages/parts/TableSettingsColumns.tsx @@ -60,14 +60,13 @@ function TableSettingsColumns(props: TempAny) { return { id: column.id, selected: column.selected, - label:
+ label:
handleSelection(index)} />
} }); - return { + return { saveColumns(items, path); setColumns(items); }} align="right" />; diff --git a/ui/src/utils/types.ts b/ui/src/utils/types.ts index c961985..3073005 100644 --- a/ui/src/utils/types.ts +++ b/ui/src/utils/types.ts @@ -46,7 +46,7 @@ export type MenuItemProps = { export type MenuProps = { "data-testid"?: string, align?: "left" | "right", - popup?: boolean, + popupId?: string, draggable?: boolean, children?: ReactNode, items: MenuItemProps[],