From a305036140696455428502e00de8a9c7baaffd25 Mon Sep 17 00:00:00 2001 From: loi Date: Mon, 26 Feb 2024 14:28:06 -0800 Subject: [PATCH 1/5] convert DialogRootContainer.DropDown --- src/firefly/js/tables/ui/TableRenderer.js | 37 +++--- src/firefly/js/ui/DialogRootContainer.jsx | 132 +++++++--------------- src/firefly/js/ui/panel/TabPanel.jsx | 60 ++++------ 3 files changed, 80 insertions(+), 149 deletions(-) diff --git a/src/firefly/js/tables/ui/TableRenderer.js b/src/firefly/js/tables/ui/TableRenderer.js index 785c8455f5..740c1f7028 100644 --- a/src/firefly/js/tables/ui/TableRenderer.js +++ b/src/firefly/js/tables/ui/TableRenderer.js @@ -5,8 +5,7 @@ import React, {useRef, useCallback, useState, useEffect} from 'react'; import {Cell} from 'fixed-data-table-2'; import {set, get, omit, isEmpty, isString, toNumber} from 'lodash'; -import {Typography, Checkbox, Stack, Box, Link, Sheet, Dropdown, Menu, MenuButton, MenuItem, IconButton, Button, Chip} from '@mui/joy'; -import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import {Typography, Checkbox, Stack, Box, Link, Sheet, MenuItem, Button, Chip} from '@mui/joy'; import {FilterInfo, FILTER_CONDITION_TTIPS, NULL_TOKEN} from '../FilterInfo.js'; import { @@ -21,7 +20,7 @@ import {toBoolean, copyToClipboard} from '../../util/WebUtil.js'; import ASC_ICO from 'html/images/sort_asc.gif'; import DESC_ICO from 'html/images/sort_desc.gif'; import {CheckboxGroupInputField} from '../../ui/CheckboxGroupInputField'; -import DialogRootContainer from '../../ui/DialogRootContainer.jsx'; +import DialogRootContainer, {DropDown} from '../../ui/DialogRootContainer.jsx'; import {FieldGroup} from '../../ui/FieldGroup'; import {getFieldVal} from '../../fieldGroup/FieldGroupUtils.js'; import {dispatchValueChange} from '../../fieldGroup/FieldGroupCntlr.js'; @@ -141,12 +140,9 @@ function Filter({cname, onFilter, filterInfo, tbl_id}) { const endDecorator = enumVals && ( - setDisableHoverListener(v) }> - - - - - + setDisableHoverListener(v) } slotProps={{button:{sx:{mr:-1}}}}> + + ); return ( @@ -329,7 +325,6 @@ export function ContentEllipsis({children, text, textAlign, sx, actions=[]}) { function ActionDropdown({text, actions, onChange}) { const popupID = 'actions--popup'; - const [open, setOpen] = useState(false); const copyCB = () => { copyToClipboard(text); }; @@ -338,16 +333,18 @@ function ActionDropdown({text, actions, onChange}) { dispatchShowDialog(popupID); }; return ( - setOpen(open) | onChange(open)}> - - - - - Copy to clipboard - View as plain text - {actions?.map((text, action) => {text})} - - + } onOpenChange={onChange} useIconButton={false} + slotProps={{ + button: { + variant:'soft', + size:'sm', + sx:{position: 'absolute', right:0, paddingInline:'.25em'} + }}} + > + Copy to clipboard + View as plain text + {actions?.map((text, action) => {text})} + ); }; diff --git a/src/firefly/js/ui/DialogRootContainer.jsx b/src/firefly/js/ui/DialogRootContainer.jsx index debaa7c616..62c7b9b2e7 100644 --- a/src/firefly/js/ui/DialogRootContainer.jsx +++ b/src/firefly/js/ui/DialogRootContainer.jsx @@ -1,18 +1,19 @@ /* * License information at https://github.com/Caltech-IPAC/firefly/blob/master/License.txt */ -import {Sheet, Stack} from '@mui/joy'; -import React, {memo, useEffect, useState} from 'react'; -import PropTypes from 'prop-types'; +import React, {memo, useEffect, useRef, useState} from 'react'; +import PropTypes, {bool, elementType, func, object, oneOfType, shape, string} from 'prop-types'; import {createRoot} from 'react-dom/client'; -import {get, set} from 'lodash'; +import {Dropdown, IconButton, Menu, MenuButton, Sheet} from '@mui/joy'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; + +import {set} from 'lodash'; import {dispatchHideDialog, isDialogVisible} from '../core/ComponentCntlr'; import {flux} from '../core/ReduxFlux'; import {FireflyRoot} from './FireflyRoot.jsx'; const DIALOG_DIV= 'dialogRootDiv'; -const DROPDOWN_DIV_ROOT= 'dropDownPlane-root'; const TMP_ROOT='TMP-'; const DEFAULT_ZINDEX= 200; @@ -24,110 +25,53 @@ let tmpCount=0; let divElement; let divElementRoot; -const reactRoots= new Map(); - function init() { divElement= createDiv({id: DIALOG_DIV}); divElementRoot= createRoot(divElement); } -/** - * locDir is a 2-digit number to indicate the location and direction of the drop-down. - * location is the first digit starting from 1-top-left to 4-bottom-left clockwise. - * direction is the 2nd digit used to denote direction. It follows the same convention as above. - * - * example: drop-down at bottom-right, spanning left. 34 - * - * @param p parameters object - * @param p.id - * @param p.content the content to display - * @param p.style overrideable style - * @param p.atElRef the element reference used to apply locDir to. - * @param p.locDir location and direction of the drop-down. see desc for more info - * @param p.wrapperStyle style to apply to dropdown wrapper div, ex. zIndex - * @param p.boxEl the container's element. Used to deactivate the dropdown. +/* + * Extend JoyUI Dropdown component to provide ease of use. + * This set focus to the popup panel on mount. This allow any click to hide it. + * @param button defaults to ArrowDropDownIcon + * @param title + * @param onOpenChange + * @param slotProps + * @param useIconButton defaults to true. more convenience than setting button.slots.root */ -export function showDropDown({id='',content, style={}, atElRef, locDir, wrapperStyle, boxEl}) { - const planeId= getddDiv(id); - const ddDiv = document.getElementById(planeId) || createDiv({id: planeId, wrapperStyle}); - const root= createRoot(ddDiv); - reactRoots.set(divElement,root); - - const rootZindex= atElRef && computeZIndex(atElRef); - if (rootZindex) ddDiv.style.zIndex ??= rootZindex; - root.render( - - - - ); - return ddDiv; -} - -export function isDropDownShowing(id) { - return document.getElementById(getddDiv(id)); -} +export function DropDown({button, title, onOpenChange, slotProps, useIconButton=true, children, ...props}) { + const [_, setOpen] = useState(false); -export function hideDropDown(id='') { - const ddDiv = document.getElementById(getddDiv(id)); - if (ddDiv) { - reactRoots.get(ddDiv)?.unmount(); - ddDiv.parentNode.removeChild(ddDiv); - } -} - -const getddDiv= (id) => id ? id+ '-dropdownPlane' : DROPDOWN_DIV_ROOT; - -function DropDown ({id, content, style={}, locDir, atElRef, boxEl}) { - - const hide = () => hideDropDown(id); - - // Effect to to hide DropDown when clicked elsewhere + const dropdownEl = useRef(null); useEffect(() => { - const box = boxEl || document; - box.addEventListener('click', hide); - return () => box.removeEventListener('click', hide);; - }, []); + dropdownEl.current?.focus(); + }, [dropdownEl.current]); - if (!get(atElRef, 'isConnected', true)) hide(); // referenced element is no longer visible.. hide drop-down. - const {x:o_x, y:o_y, width:o_width, height:o_height} = document.documentElement.getBoundingClientRect(); // outer box - const {x=0, y=0, width=0, height=0} = atElRef ? atElRef.getBoundingClientRect() : {}; // inner box + button ||= ; - - const [loc, dir] = [Math.floor(locDir/10), locDir%10]; - const top = (y-o_y) + (loc === 3 || loc === 4 ? height : 0); - const bottom = ((o_height-o_y) - y) - (loc === 3 || loc === 4 ? height : 0); - const left = (x-o_x) + (loc === 2 || loc === 3 ? width : 0); - const right = ((o_width-o_x) - x) - (loc === 2 || loc === 3 ? width : 0); - - let pos; - switch (dir) { - case 1: - pos = {bottom, right}; break; - case 2: - pos = {bottom, left}; break; - case 3: - pos = {top, left}; break; - case 4: - pos = {top, right}; break; - } - - const myStyle = Object.assign({ backgroundColor: '#FBFBFB', - ...pos, - boxShadow: '#c1c1c1 1px 1px 5px 0px', - position: 'absolute'}, - style); - const stopEvent = (e) => { - e.stopPropagation(); - e.nativeEvent && e.nativeEvent.stopImmediatePropagation(); + const onChange = (_, open) => { + onOpenChange?.(open); + setOpen(open); }; - + const root = useIconButton ? IconButton : undefined; return ( - - {content} - + + {button} + {children} + ); } +DropDown.propTypes = { + button: object, + title: oneOfType([string, elementType]), + onOpenChange: func, + useIconButton: bool, + slotProps: shape({ + button: object, + menu: object, // will inject into each one + }) +}; function requestOnTop(key) { const topKey= dialogs.sort( (d1,d2) => d2.zIndex-d1.zIndex)[0].dialogId; diff --git a/src/firefly/js/ui/panel/TabPanel.jsx b/src/firefly/js/ui/panel/TabPanel.jsx index 6ea30c34d2..2f845ed28f 100644 --- a/src/firefly/js/ui/panel/TabPanel.jsx +++ b/src/firefly/js/ui/panel/TabPanel.jsx @@ -8,7 +8,7 @@ import { Tab as JoyTab, Tabs as JoyTabs, TabPanel as JoyTabPanel, - TabList, ListItemDecorator, Box, Chip, Stack, Sheet, ChipDelete, Tooltip + TabList, ListItemDecorator, Box, Stack, Sheet, ChipDelete, Tooltip } from '@mui/joy'; import {tabClasses} from '@mui/joy/Tab'; import sizeMe from 'react-sizeme'; @@ -18,12 +18,11 @@ import {dispatchComponentStateChange, getComponentState} from '../../core/Compon import {isDefined} from '../../util/WebUtil.js'; import {useFieldGroupConnector} from '../FieldGroupConnector.jsx'; import {useStoreConnector} from '../SimpleComponent.jsx'; -import {hideDropDown, isDropDownShowing, showDropDown} from '../DialogRootContainer.jsx'; +import {DropDown} from '../DialogRootContainer.jsx'; import {TablePanel} from '../../tables/ui/TablePanel.jsx'; import {getCellValue, getTblById, watchTableChanges} from '../../tables/TableUtil.js'; import {TABLE_FILTER, TABLE_HIGHLIGHT, TABLE_SORT} from '../../tables/TablesCntlr.js'; - /*--------------------------------------------------------------------------------------------- There are several type of Tab panels, each with slightly different behavior and use case. @@ -59,17 +58,10 @@ export function TabPanel ({value, onTabSelect, tabId=uniqueTabId(), showOpenTabs const {useFlex, resizable, borderless, style={}, headerStyle, contentStyle={}, label, size, ...joyTabsProps} = rest; // these are deprecated. the rest(joyTabsProps) are pass-along props to Tabs. - const arrowEl = useRef(null); - - useEffect( ()=> { - if (isDropDownShowing(tabId)) handleOpenTabs({tabId, doOpen: false}); - }, [tabId]); - // get the content(JoyTabPanel) const childrenAry = React.Children.toArray(children); const tabContents = childrenAry.map((c, idx) => getContentFromTab(c.props, idx, slotProps)); - const showTabs = (ev) => handleOpenTabs({ev, doOpen: !isDropDownShowing(tabId), tabId, onSelect: onTabSelect, arrowEl, childrenAry}); const onChange = useCallback((ev, val) => onTabSelect?.(val), []); // because we support additional actions to the right of the TabList, we need to implement some of TabList feature here. @@ -99,9 +91,9 @@ export function TabPanel ({value, onTabSelect, tabId=uniqueTabId(), showOpenTabs {actions && actions()} {showOpenTabs && ( - -
- + + + )} @@ -390,35 +382,33 @@ function getContentFromTab({value, id, children}, idx, slotProps) { /*----------------------------------------------------------------------------------------------*/ -function handleOpenTabs({ev, doOpen, tabId, onSelect, childrenAry, arrowEl}) { - ev?.stopPropagation?.(); - if (doOpen) { - // create table model for the drop down - const columns = [{name: 'OPEN TABS', width: 50}]; - const highlightedRow = childrenAry.findIndex((child) => child?.props?.selected); - const tbl_id = tabId; - const data = getTabTitles(childrenAry); - const tableModel = {tbl_id, tableData: {columns, data}, highlightedRow, totalRows: data.length}; +function OpenTabs({tabId, onSelect, childrenAry}) { + + useEffect(() => { // monitor for changes - watchTableChanges(tabId, [TABLE_HIGHLIGHT, TABLE_SORT, TABLE_FILTER], () => { + return watchTableChanges(tabId, [TABLE_HIGHLIGHT, TABLE_SORT, TABLE_FILTER], () => { const tbl = getTblById(tabId) || {}; const {highlightedRow} = tbl; const selRowIdx = getCellValue(tbl, highlightedRow, 'ROW_IDX'); if (selRowIdx >= 0) onSelect(selRowIdx); }, tabId); // make watcherId same as tabId so there can only be one watcher per tabpanel + }, [tabId]); - const width = 381; - const content = ( - - - ); - showDropDown({id: tabId, content, atElRef: arrowEl.current, locDir: 43, - style: {marginLeft: -width+10, marginTop: -4}, wrapperStyle: {zIndex: 110}}); // 110 is the z-index of a dropdown - } else { - hideDropDown(tabId); - } -}; + // create table model for the drop down + const columns = [{name: 'OPEN TABS', width: 50}]; + const highlightedRow = childrenAry.findIndex((child) => child?.props?.selected); + const tbl_id = tabId; + const data = getTabTitles(childrenAry); + const tableModel = {tbl_id, tableData: {columns, data}, highlightedRow, totalRows: data.length}; + + const width = 381; + return ( + + + + ); +} function getTabTitles(childrenAry) { return childrenAry.map((child, idx) => { From d01d75c2b158f5d823e63d43843d76bb713ce0ab Mon Sep 17 00:00:00 2001 From: loi Date: Mon, 26 Feb 2024 14:56:36 -0800 Subject: [PATCH 2/5] Images Search: move expand/collaspe icon next to the image set label --- src/firefly/js/ui/ImageSelect.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/firefly/js/ui/ImageSelect.jsx b/src/firefly/js/ui/ImageSelect.jsx index bf5ad47f11..f488141733 100644 --- a/src/firefly/js/ui/ImageSelect.jsx +++ b/src/firefly/js/ui/ImageSelect.jsx @@ -408,7 +408,7 @@ function DataProductList({filteredImageData, groupKey, multiSelect}) { return ( - + {content} From c96fb5d2fce327b41684ec8d1d63352ff9ef8c03 Mon Sep 17 00:00:00 2001 From: loi Date: Mon, 26 Feb 2024 17:12:26 -0800 Subject: [PATCH 3/5] compact table display --- src/firefly/js/tables/TableUtil.js | 51 +++++++++++++++------ src/firefly/js/tables/ui/BasicTableView.jsx | 16 +++++-- src/firefly/js/tables/ui/TableRenderer.js | 30 ++++++++++-- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/src/firefly/js/tables/TableUtil.js b/src/firefly/js/tables/TableUtil.js index 8f90698c7d..4b497454be 100644 --- a/src/firefly/js/tables/TableUtil.js +++ b/src/firefly/js/tables/TableUtil.js @@ -1108,7 +1108,7 @@ export function tableDetailsView(tbl_id, highlightedRow, details_tbl_id) { } /** - * returns an object map of the column name and its width. + * returns an array of the value with the maximum length for each column. * The width is the number of characters needed to display * the header and the data in a table given columns and dataAry. * @param {TableColumn[]} columns array of column object @@ -1117,39 +1117,62 @@ export function tableDetailsView(tbl_id, highlightedRow, details_tbl_id) { * @param {number} opt.maxAryWidth maximum width of column with array values * @param {number} opt.maxColWidth maximum width of column without array values * @param {boolean} opt.useWidth use width and prefWidth props in calculation - * @returns {number[]} an array of widths corresponding to the given columns array. + * @returns {string[]} an array of values corresponding to the given columns array. * @memberof firefly.util.table * @func calcColumnWidths */ -export function calcColumnWidths(columns, dataAry, +export function getColMaxValues(columns, dataAry, { maxAryWidth = Number.MAX_SAFE_INTEGER, maxColWidth = Number.MAX_SAFE_INTEGER, useWidth = true, - useCnameMultiplier = false, }={}) { return columns.map( (cv, idx) => { - let width = useWidth? cv.prefWidth || cv.width : 0; + const width = useWidth? cv.prefWidth || cv.width : 0; if (width) { - return width; + return 'O'.repeat(width); // O is a good reference for average width of a character } - const cnameLength = (cv.label || cv.name)?.length * (useCnameMultiplier ? 1.25 : 1); - width = Math.max(cnameLength, get(cv, 'units.length', 0), getTypeLabel(cv).length, get(cv, 'nullString.length', 0)); + let maxVal = ''; + + // the 4 headers + [cv.label || cv.name, cv.units, getTypeLabel(cv), cv.nullString].forEach( (v) => { + if (v?.length > maxVal.length) maxVal = v; + }); + + // the data dataAry.forEach((row) => { const v = formatValue(columns[idx], row[idx]); - width = Math.max(width, v.length); + if (v.length > maxVal.length) maxVal = v; }); - if (cv.arraySize) { - width = Math.min(width, maxAryWidth); - } - width = Math.min(width, maxColWidth); - return width; + // limits + if (cv.arraySize && maxVal.length > maxAryWidth) maxVal = maxVal.substr(0, maxAryWidth); + if (maxVal.length > maxColWidth) maxVal = maxVal.substr(0, maxColWidth); + + return maxVal; }); } +/** + * returns an array of the maximum width for each column. + * The width is the number of characters needed to display + * the header and the data in a table given columns and dataAry. + * @param {TableColumn[]} columns array of column object + * @param {TableData} dataAry array of array. + * @param {object} opt options + * @param {number} opt.maxAryWidth maximum width of column with array values + * @param {number} opt.maxColWidth maximum width of column without array values + * @param {boolean} opt.useWidth use width and prefWidth props in calculation + * @returns {number[]} an array of widths corresponding to the given columns array. + * @memberof firefly.util.table + * @func calcColumnWidths + */ +export function calcColumnWidths(columns, dataAry, opt) { + return getColMaxValues(columns, dataAry, opt).map((v) => v.length); +} + /** * There are some inconsistencies in how a request is created. * This fixes any of the inconsistencies it finds. diff --git a/src/firefly/js/tables/ui/BasicTableView.jsx b/src/firefly/js/tables/ui/BasicTableView.jsx index 58d45a2c03..0bed83564f 100644 --- a/src/firefly/js/tables/ui/BasicTableView.jsx +++ b/src/firefly/js/tables/ui/BasicTableView.jsx @@ -9,11 +9,11 @@ import {Column, Table} from 'fixed-data-table-2'; import {wrapResizer} from '../../ui/SizeMeConfig.js'; import {get, set, isEmpty, isUndefined, omitBy, pick} from 'lodash'; -import {calcColumnWidths, getCellValue, getColumns, getProprietaryInfo, getTableState, getTableUiById, getTblById, hasRowAccess, isClientTable, tableTextView, TBL_STATE, uniqueTblUiId} from '../TableUtil.js'; +import {calcColumnWidths, getCellValue, getColMaxValues, getColumns, getProprietaryInfo, getTableState, getTableUiById, getTblById, hasRowAccess, isClientTable, tableTextView, TBL_STATE, uniqueTblUiId} from '../TableUtil.js'; import {SelectInfo} from '../SelectInfo.js'; import {FilterInfo} from '../FilterInfo.js'; import {SortInfo} from '../SortInfo.js'; -import {CellWrapper, HeaderCell, makeDefaultRenderer, SelectableCell, SelectableHeader} from './TableRenderer.js'; +import {CellWrapper, getPxWidth, HeaderCell, headerLevel, headerStyle, makeDefaultRenderer, SelectableCell, SelectableHeader} from './TableRenderer.js'; import {useStoreConnector} from '../../ui/SimpleComponent.jsx'; import {dispatchTableUiUpdate, TBL_UI_UPDATE} from '../TablesCntlr.js'; import {Logger} from '../../util/Logger.js'; @@ -325,8 +325,16 @@ function correctScrollLeftIfNeeded(totalColWidths, scrollLeft, width, triggeredB } function columnWidthsInPixel(columns, data) { - return calcColumnWidths(columns, data, {maxColWidth: 100, maxAryWidth: 30, useCnameMultiplier: true}) // set max width for array columns - .map( (w) => (w + 2) * 7); + + const maxVals = getColMaxValues(columns, data, {maxColWidth: 100, maxAryWidth: 30}); + + const paddings = 8; + return maxVals.map((text, idx) => { + const header = columns[idx].label || columns[idx].name; + const style = header === text ? headerStyle : {fontSize:12}; + text = text.replace(/[^a-zA-Z0-9]/g, 'O'); // some non-alphanum values can be very narrow. use 'O' in place of them. + return getPxWidth({text, ...style}) + paddings; + }); } function defHighlightedRowHandler(tbl_id, hlRowIdx, startIdx) { diff --git a/src/firefly/js/tables/ui/TableRenderer.js b/src/firefly/js/tables/ui/TableRenderer.js index 740c1f7028..c86c3de075 100644 --- a/src/firefly/js/tables/ui/TableRenderer.js +++ b/src/firefly/js/tables/ui/TableRenderer.js @@ -37,6 +37,7 @@ import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import {FilterButton} from 'firefly/visualize/ui/Buttons.jsx'; +export const headerStyle = {fontSize:'var(--joy-fontSize-sm)', fontWeight:'var(--joy-fontWeight-md)'}; // maybe faulty becuase it's translated from Typography title-sm, which is dynamic. const html_regex = /<.+>|&.+;/; // A rough detection of html elements or entities @@ -399,7 +400,9 @@ function ViewAsText({text, ...rest}) {
-