diff --git a/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx b/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx index 2ac93ddc0c..dd39fd1654 100755 --- a/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx +++ b/packages/libs/components/src/components/widgets/RadioButtonGroup.tsx @@ -50,7 +50,7 @@ export default function RadioButtonGroup({ containerStyles = {}, labelPlacement, minWidth, - buttonColor, + buttonColor = 'primary', margins, itemMarginRight, disabledList, diff --git a/packages/libs/components/src/map/DonutMarker.tsx b/packages/libs/components/src/map/DonutMarker.tsx index ddcaaea288..1d4b044767 100755 --- a/packages/libs/components/src/map/DonutMarker.tsx +++ b/packages/libs/components/src/map/DonutMarker.tsx @@ -237,7 +237,7 @@ function donutMarkerSVGIcon(props: DonutMarkerStandaloneProps): { .map((o) => o.value) .reduce((a, c) => { return a + c; - }); + }, 0); // for display, convert large value with k (e.g., 12345 -> 12k): return original value if less than a criterion const sumLabel = props.markerLabel ?? String(fullPieValue); diff --git a/packages/libs/coreui/src/components/grids/DataGrid/HeaderCell.tsx b/packages/libs/coreui/src/components/grids/DataGrid/HeaderCell.tsx index 60b075686c..1590c02ea1 100644 --- a/packages/libs/coreui/src/components/grids/DataGrid/HeaderCell.tsx +++ b/packages/libs/coreui/src/components/grids/DataGrid/HeaderCell.tsx @@ -64,11 +64,14 @@ export default function HeaderCell({ return (
= T & { __expandableTreeState: { - isSelected: boolean, - isVisible: boolean, - isIndeterminate?: boolean, - isExpanded?: boolean + isSelected: boolean; + isVisible: boolean; + isIndeterminate?: boolean; + isExpanded?: boolean; }; __expandableTreeChildren: StatefulNode[]; }; @@ -113,7 +128,6 @@ const Bar = () => | ; type ChangeHandler = (ids: string[]) => void; export type CheckboxTreeProps = { - //%%%%%%%%%%% Basic expandable tree props %%%%%%%%%%% /** Node representing root of the data to be rendered as an expandable tree */ @@ -123,7 +137,7 @@ export type CheckboxTreeProps = { getNodeId: (node: T) => string; /** Takes a node, called during rendering to provide the children for the current node */ - getNodeChildren: (node: T) => T[]; + getNodeChildren: (node: T) => T[]; /** Called when the set of expanded (branch) nodes changes. The function will be called with the array of the expanded node ids. If omitted, no handler is called. */ onExpansionChange: ChangeHandler; @@ -132,7 +146,7 @@ export type CheckboxTreeProps = { shouldExpandDescendantsWithOneChild?: boolean; /** Whether to expand a node if its contents are clicked */ - shouldExpandOnClick?: boolean + shouldExpandOnClick?: boolean; /** Whether to show the root node or start with the array of children; optional, defaults to false */ showRoot?: boolean; @@ -151,8 +165,8 @@ export type CheckboxTreeProps = { /** List of selected nodes as represented by their ids, defaults to [ ] */ selectedList: string[]; - /** - * List of filtered nodes as represented by their ids used to determine isLeafVisible node status. + /** + * List of filtered nodes as represented by their ids used to determine isLeafVisible node status. * Refer to the documentation of the createIsLeafVisible function for a better understanding of its use and behavior. * TL;DR: an empty array will render an empty tree whereas an undefined filteredList is ignored * */ @@ -252,96 +266,112 @@ type TreeLinksProps = { isFiltered: boolean; additionalActions?: React.ReactNode[]; treeLinksStyleSpec: TreeLinksStyleSpec; -} +}; /** * Renders tree links to select, clear, expand, collapse all nodes, or reset to current or default */ function TreeLinks({ - showSelectionLinks, - showExpansionLinks, - showCurrentLink, - showDefaultLink, - selectAll, - selectNone, - expandAll, - expandNone, - selectCurrentList, - selectDefaultList, - addVisible, - removeVisible, - selectOnlyVisible, - isFiltered, - additionalActions, - treeLinksStyleSpec + showSelectionLinks, + showExpansionLinks, + showCurrentLink, + showDefaultLink, + selectAll, + selectNone, + expandAll, + expandNone, + selectCurrentList, + selectDefaultList, + addVisible, + removeVisible, + selectOnlyVisible, + isFiltered, + additionalActions, + treeLinksStyleSpec, }: TreeLinksProps) { - const linkStyles = { ...treeLinksStyleSpec.links, '&:hover': linksHoverDecoration, - } + }; const filteredSelectionLinks = ( - - - - - + + + + + ); return ( -
- +
- { isFiltered && showSelectionLinks && - filteredSelectionLinks - } - { !isFiltered && showSelectionLinks && + {isFiltered && showSelectionLinks && filteredSelectionLinks} + {!isFiltered && showSelectionLinks && ( - - - - } + + + + + )} - { showExpansionLinks && + {showExpansionLinks && ( - { showSelectionLinks && } - - - - } + {showSelectionLinks && } + + + + + )} - { showSelectionLinks && showCurrentLink && + {showSelectionLinks && showCurrentLink && ( - - + + - } + )} - { showSelectionLinks && showDefaultLink && + {showSelectionLinks && showDefaultLink && ( - - + + - } - + )}
- { additionalActions && additionalActions.length > 0 && + {additionalActions && additionalActions.length > 0 && (
- { additionalActions.map((action, index, additionalActions) => ( + {additionalActions.map((action, index, additionalActions) => ( {action} - {index !== (additionalActions.length - 1) && } + {index !== additionalActions.length - 1 && } - )) } + ))}
- } - + )}
); } @@ -352,13 +382,21 @@ type ListFetcher = () => string[] | void; * Creates appropriate initial state values for a node in the stateful tree */ function getInitialNodeState(node: T, getNodeChildren: (t: T) => T[]) { - return Object.assign({}, { - // these state properties apply to all nodes - isSelected: false, isVisible: true - }, isLeaf(node, getNodeChildren) ? {} : { - // these state properties only apply to branch nodes (not leaves) - isExpanded: false, isIndeterminate: false - }) + return Object.assign( + {}, + { + // these state properties apply to all nodes + isSelected: false, + isVisible: true, + }, + isLeaf(node, getNodeChildren) + ? {} + : { + // these state properties only apply to branch nodes (not leaves) + isExpanded: false, + isIndeterminate: false, + } + ); } interface AdditionalFiltersProps { @@ -369,21 +407,19 @@ interface AdditionalFiltersProps { /** * Renders additional filters to supplement the default searchbox */ -function AdditionalFilters({ filters, filtersStyleSpec }: AdditionalFiltersProps) { +function AdditionalFilters({ + filters, + filtersStyleSpec, +}: AdditionalFiltersProps) { return ( <> - { - filters != null && filters.length > 0 && -
- { - filters.map((filter, index) => ( - - {filter} - - )) - } + {filters != null && filters.length > 0 && ( +
+ {filters.map((filter, index) => ( + {filter} + ))}
- } + )} ); } @@ -397,7 +433,7 @@ function createStatefulTree(root: T, getNodeChildren: (t: T) => T[]) { const mapFunction = (node: T, mappedChildren: StatefulNode[]) => ({ ...node, __expandableTreeChildren: mappedChildren, - __expandableTreeState: getInitialNodeState(node, getNodeChildren) + __expandableTreeState: getInitialNodeState(node, getNodeChildren), }); return mapStructure(mapFunction, getNodeChildren, root); } @@ -434,42 +470,46 @@ function applyPropsToStatefulTree( isLeafVisible: (id: string) => boolean, stateExpandedList?: string[] ) { - // if single-pick then trim selected list so at most 1 item present if (!isMultiPick && selectedList.length > 1) { - console.warn("CheckboxTree: isMultiPick = false, but more than one item selected. Ignoring all but first item."); - selectedList = [ selectedList[0] ]; + console.warn( + 'CheckboxTree: isMultiPick = false, but more than one item selected. Ignoring all but first item.' + ); + selectedList = [selectedList[0]]; } // if expanded list is null, then use default rules to determine expansion rather than explicit list - const expandedList = propsExpandedList != null ? propsExpandedList : stateExpandedList; - const expansionListProvided = (expandedList != null); + const expandedList = + propsExpandedList != null ? propsExpandedList : stateExpandedList; + const expansionListProvided = expandedList != null; const generatedExpandedList = new Set(); // convert arrays to sets for search efficiency const selectedSet = new Set(selectedList); const expandedSet = new Set(expandedList); - const mapFunction = (node: StatefulNode, mappedChildren: StatefulNode[]) => { - + const mapFunction = ( + node: StatefulNode, + mappedChildren: StatefulNode[] + ) => { const nodeId = getNodeId(node); - const { isSelected, isVisible, isExpanded, isIndeterminate } = getNodeState(node); + const { isSelected, isVisible, isExpanded, isIndeterminate } = + getNodeState(node); let newState = Object.assign({}, getNodeState(node)); let modifyThisNode = false; if (isLeaf(node, getNodeChildren)) { // only leaves can change via direct selectedness and direct visibility - const newIsSelected = (isSelectable && selectedSet.has(nodeId)); + const newIsSelected = isSelectable && selectedSet.has(nodeId); const newIsVisible = isLeafVisible(nodeId); if (newIsSelected !== isSelected || newIsVisible != isVisible) { modifyThisNode = true; newState = Object.assign(newState, { isSelected: newIsSelected, - isVisible: newIsVisible + isVisible: newIsVisible, }); } - } - else { + } else { // branches can change in all ways; first inspect children to gather information let selectedChildFound = false; let unselectedChildFound = false; @@ -484,57 +524,63 @@ function applyPropsToStatefulTree( modifyThisNode = true; } const newChildState = getNodeState(newChild); - if (newChildState.isSelected) - selectedChildFound = true; - else - unselectedChildFound = true; - if (newChildState.isIndeterminate) - indeterminateChildFound = true; - if (newChildState.isVisible) - visibleChildFound = true; + if (newChildState.isSelected) selectedChildFound = true; + else unselectedChildFound = true; + if (newChildState.isIndeterminate) indeterminateChildFound = true; + if (newChildState.isVisible) visibleChildFound = true; } // determine new state and compare with old to determine if this node should be modified - const newIsSelected = (!indeterminateChildFound && !unselectedChildFound); - const newIsIndeterminate = !newIsSelected && (indeterminateChildFound || selectedChildFound); + const newIsSelected = !indeterminateChildFound && !unselectedChildFound; + const newIsIndeterminate = + !newIsSelected && (indeterminateChildFound || selectedChildFound); const newIsVisible = visibleChildFound; - const newIsExpanded = (isActiveSearch(isAdditionalFilterApplied, isSearchable, searchTerm) && newIsVisible) || - (expansionListProvided ? expandedSet.has(nodeId) : - (indeterminateChildFound || (selectedChildFound && (!isMultiPick || unselectedChildFound)))); + const newIsExpanded = + (isActiveSearch(isAdditionalFilterApplied, isSearchable, searchTerm) && + newIsVisible) || + (expansionListProvided + ? expandedSet.has(nodeId) + : indeterminateChildFound || + (selectedChildFound && (!isMultiPick || unselectedChildFound))); if (!expansionListProvided && newIsExpanded) { generatedExpandedList.add(nodeId); } - if (modifyThisNode || - newIsSelected !== isSelected || - newIsIndeterminate !== isIndeterminate || - newIsExpanded !== isExpanded || - newIsVisible !== isVisible) { + if ( + modifyThisNode || + newIsSelected !== isSelected || + newIsIndeterminate !== isIndeterminate || + newIsExpanded !== isExpanded || + newIsVisible !== isVisible + ) { modifyThisNode = true; newState = Object.assign(newState, { isSelected: newIsSelected, isVisible: newIsVisible, isIndeterminate: newIsIndeterminate, - isExpanded: newIsExpanded + isExpanded: newIsExpanded, }); } } // return the existing node if no changes present in this or children; otherwise create new - return !modifyThisNode ? node + return !modifyThisNode + ? node : Object.assign({}, node, { - [NODE_CHILDREN_PROPERTY]: mappedChildren, - [NODE_STATE_PROPERTY]: newState - }); - } + [NODE_CHILDREN_PROPERTY]: mappedChildren, + [NODE_STATE_PROPERTY]: newState, + }); + }; // generate the new stateful tree, and expanded list (if necessary) const newStatefulTree = mapStructure(mapFunction, getStatefulChildren, root); return { // convert whichever Set we want back to an array - expandedList: Array.from(expansionListProvided ? expandedSet : generatedExpandedList), - statefulTree: newStatefulTree + expandedList: Array.from( + expansionListProvided ? expandedSet : generatedExpandedList + ), + statefulTree: newStatefulTree, }; } @@ -545,7 +591,7 @@ function applyPropsToStatefulTree( function isActiveSearch( isAdditionalFilterApplied: CheckboxTreeProps['isAdditionalFilterApplied'], isSearchable: CheckboxTreeProps['isSearchable'], - searchTerm: CheckboxTreeProps['searchTerm'], + searchTerm: CheckboxTreeProps['searchTerm'] ) { return isSearchable && isFiltered(searchTerm, isAdditionalFilterApplied); } @@ -556,10 +602,7 @@ function isActiveSearch( * 2. an "additional filter" has been applied */ function isFiltered(searchTerm: string, isAdditionalFilterApplied?: boolean) { - return ( - searchTerm.length > 0 || - Boolean(isAdditionalFilterApplied) - ); + return searchTerm.length > 0 || Boolean(isAdditionalFilterApplied); } /** @@ -572,11 +615,11 @@ function isFiltered(searchTerm: string, isAdditionalFilterApplied?: boolean) { * If a search is being actively performed, then matching nodes, their children, and * their ancestors will be visible (expansion is locked and all branches are * expanded). - * + * * The filteredList prop is only applied to leaves. An important "gotcha" to consider * is this: passing an empty array will render no leaves based on leaf filtering logic. * If that is not desired, pass in an undefined filteredList prop instead of an empty array. - * + * * The function returned by createIsLeafVisible does not care about branches, but tells * absolutely if a leaf should be visible (i.e. if the leaf matches the search or if any * ancestor matches the search). @@ -589,10 +632,13 @@ function createIsLeafVisible( getNodeChildren: CheckboxTreeProps['getNodeChildren'], isAdditionalFilterApplied: CheckboxTreeProps['isAdditionalFilterApplied'], isSearchable: CheckboxTreeProps['isSearchable'], - filteredList: CheckboxTreeProps['filteredList'], + filteredList: CheckboxTreeProps['filteredList'] ) { // if not searching, if no additional filters are applied, and if filteredList is undefined, then all nodes are visible - if (!isActiveSearch(isAdditionalFilterApplied, isSearchable, searchTerm) && !filteredList) { + if ( + !isActiveSearch(isAdditionalFilterApplied, isSearchable, searchTerm) && + !filteredList + ) { return (nodeId: string) => true; } // otherwise must construct array of visible leaves @@ -607,7 +653,7 @@ function createIsLeafVisible( nodeMatches = parentMatches; } else { // handles filtering by search only - nodeMatches = searchPredicate(node, searchTerms) + nodeMatches = searchPredicate(node, searchTerms); } if (isLeaf(node, getNodeChildren)) { @@ -617,13 +663,12 @@ function createIsLeafVisible( visibleLeaves.add(nodeId); } } - } - else { - getNodeChildren(node).forEach(child => { + } else { + getNodeChildren(node).forEach((child) => { addVisibleLeaves(child, nodeMatches); }); } - } + }; addVisibleLeaves(tree, false); return (nodeId: string) => visibleLeaves.has(nodeId); } @@ -646,311 +691,380 @@ function getNodeState(node: StatefulNode) { /** * Expandable tree component */ -function CheckboxTree (props: CheckboxTreeProps) { - const { - tree, - getNodeId, - getNodeChildren, - searchTerm, - selectedList, - currentList, - defaultList, - isSearchable, - isAdditionalFilterApplied, - name, - shouldExpandDescendantsWithOneChild, - onExpansionChange, - isSelectable, - isMultiPick, - onSelectionChange, - showRoot, - additionalActions, - linksPosition = LinksPosition.Both, - showSearchBox, - autoFocusSearchBox, - onSearchTermChange, - searchBoxPlaceholder, - searchIconName, - searchIconPosition, - searchBoxHelp, - additionalFilters, - wrapTreeSection, - shouldExpandOnClick = true, - customCheckboxes, - renderNoResults, - styleOverrides = {}, - customTreeNodeCssSelectors = {}, - renderNode: renderNodeProp - } = props; - - const styleSpec: CheckboxTreeStyleSpec = useMemo(() => { - return merge({}, defaultCheckboxTreeStyleSpec, styleOverrides) - }, [styleOverrides]) - - // initialize stateful tree; this immutable tree structure will be replaced with each state change - const treeState = useTreeState(props); - - /** - * Creates a function that will handle a click of one of the tree links above - */ - function createLinkHandler( - idListFetcher: ListFetcher, - changeHandler: ChangeHandler - ): TreeLinkHandler { - return function (event) { - // prevent update to URL - event.preventDefault(); - - // call instance's change handler with the appropriate ids - const idList = idListFetcher(); - if (idList !== undefined && idList !== null) { - changeHandler(idList); - } - }; - } - - /** - * Creates a function that will handle expansion-related tree link clicks - */ - function createExpander(listFetcher: ListFetcher) { - return createLinkHandler(listFetcher, props.onExpansionChange); - } - - /** - * Creates a function that will handle selection-related tree link clicks - */ - function createSelector(listFetcher: ListFetcher) { - return createLinkHandler(listFetcher, props.onSelectionChange); - } +function CheckboxTree(props: CheckboxTreeProps) { + const { + tree, + getNodeId, + getNodeChildren, + searchTerm, + selectedList, + currentList, + defaultList, + isSearchable, + isAdditionalFilterApplied, + name, + shouldExpandDescendantsWithOneChild, + onExpansionChange, + isSelectable, + isMultiPick, + onSelectionChange, + showRoot, + additionalActions, + linksPosition = LinksPosition.Both, + showSearchBox, + autoFocusSearchBox, + onSearchTermChange, + searchBoxPlaceholder, + searchIconName, + searchIconPosition, + searchBoxHelp, + additionalFilters, + wrapTreeSection, + shouldExpandOnClick = true, + customCheckboxes, + renderNoResults, + styleOverrides = {}, + customTreeNodeCssSelectors = {}, + renderNode: renderNodeProp, + } = props; + + const styleSpec: CheckboxTreeStyleSpec = useMemo(() => { + return merge({}, defaultCheckboxTreeStyleSpec, styleOverrides); + }, [styleOverrides]); + + // initialize stateful tree; this immutable tree structure will be replaced with each state change + const treeState = useTreeState(props); - // define event handlers related to expansion - const expandAll = createExpander(() => getBranches(tree, getNodeChildren).map(node => getNodeId(node))); - const expandNone = createExpander(() => []); + /** + * Creates a function that will handle a click of one of the tree links above + */ + function createLinkHandler( + idListFetcher: ListFetcher, + changeHandler: ChangeHandler + ): TreeLinkHandler { + return function (event) { + // prevent update to URL + event.preventDefault(); + + // call instance's change handler with the appropriate ids + const idList = idListFetcher(); + if (idList !== undefined && idList !== null) { + changeHandler(idList); + } + }; + } - // define event handlers related to selection + /** + * Creates a function that will handle expansion-related tree link clicks + */ + function createExpander(listFetcher: ListFetcher) { + return createLinkHandler(listFetcher, props.onExpansionChange); + } - // add all nodes to selectedList - const selectAll = createSelector(() => - getLeaves(tree, getNodeChildren).map(getNodeId)); + /** + * Creates a function that will handle selection-related tree link clicks + */ + function createSelector(listFetcher: ListFetcher) { + return createLinkHandler(listFetcher, props.onSelectionChange); + } + + // define event handlers related to expansion + const expandAll = createExpander(() => + getBranches(tree, getNodeChildren).map((node) => getNodeId(node)) + ); + const expandNone = createExpander(() => []); - // remove all nodes from selectedList - const selectNone = createSelector(() => []); + // define event handlers related to selection + + // add all nodes to selectedList + const selectAll = createSelector(() => + getLeaves(tree, getNodeChildren).map(getNodeId) + ); - // add visible nodes to selectedList - const addVisible = createSelector(() => - Seq.from(selectedList) - .concat(getLeaves(tree, getNodeChildren) + // remove all nodes from selectedList + const selectNone = createSelector(() => []); + + // add visible nodes to selectedList + const addVisible = createSelector(() => + Seq.from(selectedList) + .concat( + getLeaves(tree, getNodeChildren) .map(getNodeId) - .filter(treeState.isLeafVisible)) - .uniq() - .toArray()); + .filter(treeState.isLeafVisible) + ) + .uniq() + .toArray() + ); - // set selected list to only visible nodes - const selectOnlyVisible = createSelector(() => - getLeaves(tree, getNodeChildren) + // set selected list to only visible nodes + const selectOnlyVisible = createSelector(() => + getLeaves(tree, getNodeChildren) .map(getNodeId) - .filter(treeState.isLeafVisible)); - - // remove visible nodes from selectedList - const removeVisible = createSelector(() => - selectedList - .filter(nodeId => !treeState.isLeafVisible(nodeId))); - - - const selectCurrentList = createSelector(() => currentList); - const selectDefaultList = createSelector(() => defaultList); - - /** - * Toggle expansion of the given node. If node is a leaf, does nothing. - */ - const toggleExpansion = useCallback((node: T) => { - if (!isActiveSearch(isAdditionalFilterApplied, isSearchable, searchTerm) && !isLeaf(node, getNodeChildren)) { - if (!shouldExpandDescendantsWithOneChild || treeState.generated.expandedList.includes(getNodeId(node))) { - // If "shouldExpandDescendantsWithOneChild" is not set to "true," or the node is already expanded, - // simply addOrRemove the node to/from the expandedList - onExpansionChange(addOrRemove(treeState.generated.expandedList, getNodeId(node))); - } else { - // Otherwise, add the node and its descendants with one child to the expandedList - const descendantNodesWithOneChild = _findDescendantsWithOneChild(node); - - const newExpandedList = Seq.from(treeState.generated.expandedList) + .filter(treeState.isLeafVisible) + ); + + // remove visible nodes from selectedList + const removeVisible = createSelector(() => + selectedList.filter((nodeId) => !treeState.isLeafVisible(nodeId)) + ); + + const selectCurrentList = createSelector(() => currentList); + const selectDefaultList = createSelector(() => defaultList); + + /** + * Toggle expansion of the given node. If node is a leaf, does nothing. + */ + const toggleExpansion = useCallback( + (node: T) => { + if ( + !isActiveSearch(isAdditionalFilterApplied, isSearchable, searchTerm) && + !isLeaf(node, getNodeChildren) + ) { + if ( + !shouldExpandDescendantsWithOneChild || + treeState.generated.expandedList.includes(getNodeId(node)) + ) { + // If "shouldExpandDescendantsWithOneChild" is not set to "true," or the node is already expanded, + // simply addOrRemove the node to/from the expandedList + onExpansionChange( + addOrRemove(treeState.generated.expandedList, getNodeId(node)) + ); + } else { + // Otherwise, add the node and its descendants with one child to the expandedList + const descendantNodesWithOneChild = + _findDescendantsWithOneChild(node); + + const newExpandedList = Seq.from(treeState.generated.expandedList) .concat(descendantNodesWithOneChild) .uniq() .toArray(); - onExpansionChange(newExpandedList); - } + onExpansionChange(newExpandedList); } - function _findDescendantsWithOneChild(descendant: T): Seq { - const nextNodes = getNodeId(node) === getNodeId(descendant) || getNodeChildren(descendant).length === 1 - ? Seq.from([ getNodeId(descendant) ]) + } + function _findDescendantsWithOneChild(descendant: T): Seq { + const nextNodes = + getNodeId(node) === getNodeId(descendant) || + getNodeChildren(descendant).length === 1 + ? Seq.from([getNodeId(descendant)]) : Seq.empty(); - - const remainingNodes = Seq.from(getNodeChildren(descendant)).flatMap(_findDescendantsWithOneChild); - - return nextNodes.concat(remainingNodes); - } - }, [getNodeChildren, getNodeId, isAdditionalFilterApplied, isSearchable, onExpansionChange, searchTerm, shouldExpandDescendantsWithOneChild, treeState.generated.expandedList]); + const remainingNodes = Seq.from(getNodeChildren(descendant)).flatMap( + _findDescendantsWithOneChild + ); - /** + return nextNodes.concat(remainingNodes); + } + }, + [ + getNodeChildren, + getNodeId, + isAdditionalFilterApplied, + isSearchable, + onExpansionChange, + searchTerm, + shouldExpandDescendantsWithOneChild, + treeState.generated.expandedList, + ] + ); + + /** * Toggle selection of the given node. * If toggled checkbox is a selected leaf - add the leaf to the select list to be returned * If toggled checkbox is an unselected leaf - remove the leaf from the select list to be returned * If toggled checkbox is a selected non-leaf - identify the node's leaves and add them to the select list to be returned * If toggled checkbox is an unselected non-leaf - identify the node's leaves and remove them from the select list to be returned */ - const toggleSelection = useCallback((node: T, selected: boolean) => { - if (!isSelectable) return; - if (isLeaf(node, getNodeChildren)) { - if (isMultiPick) { - onSelectionChange(addOrRemove(selectedList, getNodeId(node))); - } - else { - // radio button will only fire if changing from unselected -> selected; - // if single-pick, any event means only the clicked node is the new list - onSelectionChange([ getNodeId(node) ]); - } + const toggleSelection = useCallback( + (node: T, selected: boolean) => { + if (!isSelectable) return; + if (isLeaf(node, getNodeChildren)) { + if (isMultiPick) { + onSelectionChange(addOrRemove(selectedList, getNodeId(node))); + } else { + // radio button will only fire if changing from unselected -> selected; + // if single-pick, any event means only the clicked node is the new list + onSelectionChange([getNodeId(node)]); } - else { - const newSelectedList = (selectedList ? selectedList.slice() : []); - const leafNodes = getLeaves(node, getNodeChildren); - leafNodes.forEach(leafNode => { - const leafId = getNodeId(leafNode); - const index = newSelectedList.indexOf(leafId); - if (selected && index === -1) { - newSelectedList.push(leafId); - } - else if (!selected && index > -1) { - newSelectedList.splice(index, 1); - } - }); - onSelectionChange(newSelectedList); - } - }, [getNodeChildren, getNodeId, isMultiPick, isSelectable, onSelectionChange, selectedList]); - - const renderNode = useCallback((node: T, path?: number[]) => { - return renderNodeProp - ? renderNodeProp(node, path) - : {getNodeId(node)} - }, [getNodeId, renderNodeProp]); - - const topLevelNodes = (showRoot ? [ treeState.generated.statefulTree ] : - getStatefulChildren(treeState.generated.statefulTree)); - - const isTreeVisible = treeState.generated && getNodeState(treeState.generated.statefulTree).isVisible; - const noResultsRenderFunction = renderNoResults || defaultRenderNoResults; - const noResultsMessage = isTreeVisible ? null : noResultsRenderFunction(searchTerm, tree); - - const treeLinks = ( - - ); - - const treeNodeCssSelectors = useMemo(() => { - return ({ - '.list': styleSpec.treeNode?.list, - '.visible-element': { display: '' }, - '.hidden-element': { display: 'none' }, - '.node-wrapper': {...styleSpec.treeNode?.nodeWrapper}, - '.top-level-node-wrapper': {...styleSpec.treeNode?.nodeWrapper, ...styleSpec.treeNode?.topLevelNodeWrapper}, - '.arrow-icon': { fill: '#aaa', fontSize: '0.75em', cursor: 'pointer' }, - '.label-text-wrapper': { ...styleSpec.treeNode?.labelTextWrapper }, - '.leaf-node-label': { ...styleSpec.treeNode?.leafNodeLabel }, - '.node-label': { ...styleSpec.treeNode?.nodeLabel }, - '.children': styleSpec.treeNode?.children, - '.active-search-buffer': { width: '0.75em' }, - ...customTreeNodeCssSelectors - }) - }, [styleSpec.treeNode, customTreeNodeCssSelectors]) - - const treeSection = ( -
-
    - {topLevelNodes.map((node, index) => { - const nodeId = getNodeId(node); - - return ( - >} - isTopLevelNode={true} - /> - ) - }) + } else { + const newSelectedList = selectedList ? selectedList.slice() : []; + const leafNodes = getLeaves(node, getNodeChildren); + leafNodes.forEach((leafNode) => { + const leafId = getNodeId(leafNode); + const index = newSelectedList.indexOf(leafId); + if (selected && index === -1) { + newSelectedList.push(leafId); + } else if (!selected && index > -1) { + newSelectedList.splice(index, 1); } -
-
- ) - - return ( - <> - {linksPosition && linksPosition == LinksPosition.Top ? treeLinks : null} - {!isSearchable || !showSearchBox ? "" : ( -
- - { + return renderNodeProp ? ( + renderNodeProp(node, path) + ) : ( + {getNodeId(node)} + ); + }, + [getNodeId, renderNodeProp] + ); + + const topLevelNodes = showRoot + ? [treeState.generated.statefulTree] + : getStatefulChildren(treeState.generated.statefulTree); + + const isTreeVisible = + treeState.generated && + getNodeState(treeState.generated.statefulTree).isVisible; + const noResultsRenderFunction = renderNoResults || defaultRenderNoResults; + const noResultsMessage = isTreeVisible + ? null + : noResultsRenderFunction(searchTerm, tree); + + const treeLinks = ( + + ); + + const treeNodeCssSelectors = useMemo(() => { + return { + '.list': styleSpec.treeNode?.list, + '.visible-element': { display: '' }, + '.hidden-element': { display: 'none' }, + '.node-wrapper': { ...styleSpec.treeNode?.nodeWrapper }, + '.top-level-node-wrapper': { + ...styleSpec.treeNode?.nodeWrapper, + ...styleSpec.treeNode?.topLevelNodeWrapper, + }, + '.arrow-container': { + height: '1em', + }, + '.arrow-icon': { + fill: '#aaa', + fontSize: '0.75em', + cursor: 'pointer', + }, + '.label-text-wrapper': { ...styleSpec.treeNode?.labelTextWrapper }, + '.leaf-node-label': { ...styleSpec.treeNode?.leafNodeLabel }, + '.node-label': { ...styleSpec.treeNode?.nodeLabel }, + '.children': styleSpec.treeNode?.children, + '.active-search-buffer': { width: '0.75em' }, + ...customTreeNodeCssSelectors, + }; + }, [styleSpec.treeNode, customTreeNodeCssSelectors]); + + const treeSection = ( +
+
    + {topLevelNodes.map((node, index) => { + const nodeId = getNodeId(node); + + return ( + > + } + isTopLevelNode={true} /> -
- )} - {noResultsMessage} - {wrapTreeSection ? wrapTreeSection(treeSection) : treeSection} - {linksPosition && linksPosition == LinksPosition.Bottom ? treeLinks : null} - - ); + ); + })} + +
+ ); + + return ( + <> + {linksPosition && linksPosition == LinksPosition.Top ? treeLinks : null} + {!isSearchable || !showSearchBox ? ( + '' + ) : ( +
+ + +
+ )} + {noResultsMessage} + {wrapTreeSection ? wrapTreeSection(treeSection) : treeSection} + {linksPosition && linksPosition == LinksPosition.Bottom + ? treeLinks + : null} + + ); } function defaultRenderNoResults() { return ( -
- - +
+ + The search term you entered did not yield any results.
@@ -964,22 +1078,25 @@ const defaultProps = { selectedList: [], customCheckboxes: {}, isMultiPick: true, - onSelectionChange: () => {/* */}, + onSelectionChange: () => { + /* */ + }, isSearchable: false, showSearchBox: true, - searchBoxPlaceholder: "Search...", + searchBoxPlaceholder: 'Search...', searchBoxHelp: '', searchTerm: '', - onSearchTermChange: () => {/* */}, + onSearchTermChange: () => { + /* */ + }, searchPredicate: () => true, - linksPosition: LinksPosition.Both + linksPosition: LinksPosition.Both, }; CheckboxTree.defaultProps = defaultProps; CheckboxTree.LinkPlacement = LinksPosition; export default CheckboxTree; - function useTreeState(props: CheckboxTreeProps) { const { tree, @@ -993,9 +1110,12 @@ function useTreeState(props: CheckboxTreeProps) { isMultiPick, selectedList, expandedList, - filteredList + filteredList, } = props; - const statefulTree = useMemo(() => createStatefulTree(tree, getNodeChildren), [tree, getNodeChildren]); + const statefulTree = useMemo( + () => createStatefulTree(tree, getNodeChildren), + [tree, getNodeChildren] + ); // initialize stateful tree; this immutable tree structure will be replaced with each state change const makeTreeState = useCallback(() => { @@ -1007,7 +1127,7 @@ function useTreeState(props: CheckboxTreeProps) { getNodeChildren, isAdditionalFilterApplied, isSearchable, - filteredList, + filteredList ); const generatedTreeState = applyPropsToStatefulTree( statefulTree, @@ -1026,8 +1146,22 @@ function useTreeState(props: CheckboxTreeProps) { return { isLeafVisible, generated: generatedTreeState, - } - }, [tree, searchTerm, searchPredicate, getNodeId, getNodeChildren, isAdditionalFilterApplied, isSearchable, statefulTree, isSelectable, isMultiPick, selectedList, expandedList, filteredList]); + }; + }, [ + tree, + searchTerm, + searchPredicate, + getNodeId, + getNodeChildren, + isAdditionalFilterApplied, + isSearchable, + statefulTree, + isSelectable, + isMultiPick, + selectedList, + expandedList, + filteredList, + ]); const [treeState, setTreeState] = useState(makeTreeState); @@ -1039,11 +1173,11 @@ function useTreeState(props: CheckboxTreeProps) { const timerId = setTimeout(performUpdate, 250); return function cancel() { clearTimeout(timerId); - } + }; } else { performUpdate(); } - }, [makeTreeState, searchTerm]) + }, [makeTreeState, searchTerm]); return treeState; } diff --git a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx index ced4933a4e..cdbad9a63f 100644 --- a/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx +++ b/packages/libs/coreui/src/components/inputs/checkboxes/CheckboxTree/CheckboxTreeNode.tsx @@ -1,16 +1,18 @@ import React from 'react'; import { isLeaf } from '../../SelectTree/Utils'; -import IndeterminateCheckbox, { IndeterminateCheckboxProps } from '../IndeterminateCheckbox'; +import IndeterminateCheckbox, { + IndeterminateCheckboxProps, +} from '../IndeterminateCheckbox'; import { ArrowRight, ArrowDown } from '../../../icons'; export type CheckboxTreeNodeStyleSpec = { list?: { - listStyle: React.CSSProperties['listStyle'], - }, + listStyle: React.CSSProperties['listStyle']; + }; children?: { - padding: React.CSSProperties['padding'] - margin: React.CSSProperties['margin'] - }, + padding: React.CSSProperties['padding']; + margin: React.CSSProperties['margin']; + }; nodeWrapper?: React.CSSProperties; topLevelNodeWrapper?: React.CSSProperties; leafNodeLabel?: React.CSSProperties; @@ -28,7 +30,7 @@ export const defaultTreeNodeStyleSpec: CheckboxTreeNodeStyleSpec = { }, nodeWrapper: { display: 'flex', - alignItems: 'center', + alignItems: 'start', padding: '1px 0', }, topLevelNodeWrapper: {}, @@ -36,18 +38,20 @@ export const defaultTreeNodeStyleSpec: CheckboxTreeNodeStyleSpec = { display: 'flex', width: '100%', marginLeft: '1.25em', + alignItems: 'start', }, nodeLabel: { display: 'flex', width: '100%', marginLeft: '0.5em', + alignItems: 'start', }, labelTextWrapper: { - width: '100%', + width: '100%', margin: 'auto 0', paddingLeft: '0.25em', - } -} + }, +}; type TreeRadioProps = { name: string; @@ -55,31 +59,30 @@ type TreeRadioProps = { value: string; node: T; onChange: (node: T, checked: boolean) => void; -} +}; function TreeRadio({ - name, - checked, - value, - node, - onChange + name, + checked, + value, + node, + onChange, }: TreeRadioProps) { - - const handleClick = () => { - if (!checked) { - onChange(node, false); - } - }; + const handleClick = () => { + if (!checked) { + onChange(node, false); + } + }; - return ( - - ) + return ( + + ); } type NodeState = { @@ -87,9 +90,11 @@ type NodeState = { isVisible: boolean; isIndeterminate?: boolean; isExpanded?: boolean; -} +}; -export type CustomCheckboxes = {[index: string]: React.ComponentType>>}; +export type CustomCheckboxes = { + [index: string]: React.ComponentType>>; +}; type Props = { node: T; @@ -107,126 +112,133 @@ type Props = { customCheckboxes?: CustomCheckboxes; shouldExpandOnClick: boolean; isTopLevelNode?: boolean; -} +}; export default function CheckboxTreeNode({ - name, + name, + node, + path, + getNodeState, + isSelectable, + isMultiPick, + isActiveSearch, + toggleSelection, + toggleExpansion, + getNodeId, + getNodeChildren, + renderNode, + customCheckboxes, + shouldExpandOnClick, + isTopLevelNode = false, +}: Props) { + let { isSelected, isIndeterminate, isVisible, isExpanded } = + getNodeState(node); + let isLeafNode = isLeaf(node, getNodeChildren); + let inputName = isLeafNode ? name : ''; + let nodeId = getNodeId(node); + const nodeElement = renderNode(node, path.split('/').map(Number)); + const commonInputProps = { + name: inputName, + checked: isSelected, node, - path, - getNodeState, - isSelectable, - isMultiPick, - isActiveSearch, - toggleSelection, - toggleExpansion, - getNodeId, - getNodeChildren, - renderNode, - customCheckboxes, - shouldExpandOnClick, - isTopLevelNode = false, - }: Props -) { + value: nodeId, + }; + const checkboxProps: IndeterminateCheckboxProps = { + ...commonInputProps, + indeterminate: !!isIndeterminate, + onChange: (isChecked: boolean) => toggleSelection(node, isChecked), + }; + const CustomCheckbox = + customCheckboxes && nodeId in customCheckboxes + ? customCheckboxes[nodeId] + : undefined; - let { isSelected, isIndeterminate, isVisible, isExpanded } = getNodeState(node); - let isLeafNode = isLeaf(node, getNodeChildren); - let inputName = isLeafNode ? name : ''; - let nodeId = getNodeId(node); - const nodeElement = renderNode(node, path.split('/').map(Number)); - const commonInputProps = { - name: inputName, - checked: isSelected, - node, - value: nodeId, - }; - const checkboxProps: IndeterminateCheckboxProps = {...commonInputProps, indeterminate: !!isIndeterminate, onChange: (isChecked: boolean) => toggleSelection(node, isChecked) }; - const CustomCheckbox = (customCheckboxes && (nodeId in customCheckboxes)) ? customCheckboxes[nodeId] : undefined; - - return ( -
  • +
    -
    - {isLeafNode ? null - : isActiveSearch ? ( - // this retains the space of the expansion toggle icons for easier formatting -
    - ) : ( - isExpanded ? - { - e.stopPropagation(); - toggleExpansion(node); - }} - onKeyDown={(e) => e.key === 'Enter' ? toggleExpansion(node) : null} - /> : - { - e.stopPropagation(); - toggleExpansion(node); - }} - onKeyDown={(e) => e.key === 'Enter' ? toggleExpansion(node) : null} - /> - )} - {!isSelectable || (!isMultiPick && !isLeafNode) ? ( -
    toggleExpansion(node) : undefined} - > - {nodeElement} -
    - ) : ( - - )} -
    - { !isLeafNode && isVisible && isExpanded && -
      + ) : isExpanded ? ( +
      + { + e.stopPropagation(); + toggleExpansion(node); + }} + onKeyDown={(e) => + e.key === 'Enter' ? toggleExpansion(node) : null + } + /> +
      + ) : ( +
      + { + e.stopPropagation(); + toggleExpansion(node); + }} + onKeyDown={(e) => + e.key === 'Enter' ? toggleExpansion(node) : null + } + /> +
      + )} + {!isSelectable || (!isMultiPick && !isLeafNode) ? ( +
      toggleExpansion(node) : undefined + } > - {getNodeChildren(node).map((child, index) => - + {nodeElement} +
      + ) : ( +
    • - ); -} \ No newline at end of file +
      {nodeElement}
      + + )} +
      + {!isLeafNode && isVisible && isExpanded && ( +
        + {getNodeChildren(node).map((child, index) => ( + + ))} +
      + )} + + ); +} diff --git a/packages/libs/eda/src/lib/core/components/filter/HistogramFilter.tsx b/packages/libs/eda/src/lib/core/components/filter/HistogramFilter.tsx index 83fb185148..fd195ec5ab 100755 --- a/packages/libs/eda/src/lib/core/components/filter/HistogramFilter.tsx +++ b/packages/libs/eda/src/lib/core/components/filter/HistogramFilter.tsx @@ -277,12 +277,13 @@ export function HistogramFilter(props: Props) { const updateUIState = useCallback( (newUiState: Partial) => { // if (uiState.binWidth === newUiState.binWidth) return; - analysisState.setVariableUISettings({ + analysisState.setVariableUISettings((currentState) => ({ + ...currentState, [uiStateKey]: { ...uiState, ...newUiState, }, - }); + })); }, [analysisState, uiStateKey, uiState] ); diff --git a/packages/libs/eda/src/lib/core/components/variableTrees/VariableList.tsx b/packages/libs/eda/src/lib/core/components/variableTrees/VariableList.tsx index 7d382b3221..9ed20b110b 100644 --- a/packages/libs/eda/src/lib/core/components/variableTrees/VariableList.tsx +++ b/packages/libs/eda/src/lib/core/components/variableTrees/VariableList.tsx @@ -54,7 +54,7 @@ import useUITheme from '@veupathdb/coreui/dist/components/theming/useUITheme'; import { VariableLink, VariableLinkConfig } from '../VariableLink'; const baseFieldNodeLinkStyle = { - padding: '0.25em 0.5em', + padding: '0 0.5em', borderRadius: '0.5em', display: 'inline-block', cursor: 'pointer', @@ -87,7 +87,11 @@ const useFieldNodeCssSelectors = () => { ...baseFieldNodeLinkStyle, ...activeFieldNodeLinkStyle, }, - '.single-select-anchor-node': { marginLeft: '0.5em' }, + '.single-select-anchor-node': { + marginLeft: '0.5em', + alignSelf: 'center', + padding: '0.25em 0.5em', + }, '.dropdown-node-color': { color: '#2f2f2f' }, '.base-node-color': { color: themePrimaryColor ?? '#069', @@ -95,7 +99,7 @@ const useFieldNodeCssSelectors = () => { '.entity-node': { fontWeight: 'bold', cursor: 'pointer', - padding: '0.25em 0.5em', + padding: '0 0.5em', }, '.starred-var-container': { display: 'flex', @@ -719,6 +723,7 @@ export default function VariableList({ display: 'flex', alignItems: 'center', marginLeft: '1em', + padding: scope === 'download' ? '0.2em 0' : undefined, }} > {isMultiPick && @@ -794,7 +799,10 @@ export default function VariableList({ styleOverrides: { treeNode: { nodeWrapper: { - padding: 0, + padding: scope === 'download' ? '0.125em 0' : 0, + }, + topLevelNodeWrapper: { + padding: '0.25em 0.5em', }, }, treeLinks: { diff --git a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx index b5f519ca2e..eb1ba39ff1 100755 --- a/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx +++ b/packages/libs/eda/src/lib/core/components/visualizations/implementations/BoxplotVisualization.tsx @@ -245,11 +245,16 @@ function BoxplotViz(props: VisualizationProps) { const findEntityAndVariable = useFindEntityAndVariable(filters); - const providedXAxisVariable = options?.getXAxisVariable?.( - computation.descriptor.configuration + const { getXAxisVariable, getComputedYAxisDetails } = options ?? {}; + const { configuration: computeConfig } = computation.descriptor; + + const providedXAxisVariable = useMemo( + () => getXAxisVariable?.(computeConfig), + [computeConfig, getXAxisVariable] ); - const computedYAxisDetails = options?.getComputedYAxisDetails?.( - computation.descriptor.configuration + const computedYAxisDetails = useMemo( + () => getComputedYAxisDetails?.(computeConfig), + [computeConfig, getComputedYAxisDetails] ); // When we only have a computed y axis (and no provided x axis) then the y axis var diff --git a/packages/libs/eda/src/lib/core/hooks/entityCounts.ts b/packages/libs/eda/src/lib/core/hooks/entityCounts.ts index 6704370bb5..30c15d976f 100644 --- a/packages/libs/eda/src/lib/core/hooks/entityCounts.ts +++ b/packages/libs/eda/src/lib/core/hooks/entityCounts.ts @@ -1,16 +1,20 @@ -import { preorder } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; import { useCallback } from 'react'; import { Filter } from '../types/filter'; import { usePromise } from './promise'; -import { useStudyMetadata, useSubsettingClient } from './workspace'; +import { + useStudyEntities, + useStudyMetadata, + useSubsettingClient, +} from './workspace'; import { useDebounce } from '../hooks/debouncing'; import { isStubEntity, STUB_ENTITY } from './study'; export type EntityCounts = Record; export function useEntityCounts(filters?: Filter[]) { - const { id, rootEntity } = useStudyMetadata(); const subsettingClient = useSubsettingClient(); + const { id, rootEntity } = useStudyMetadata(); + const entities = useStudyEntities(); // debounce to prevent a back end call for each click on a filter checkbox const debouncedFilters = useDebounce(filters, 2000); @@ -21,22 +25,25 @@ export function useEntityCounts(filters?: Filter[]) { return { [STUB_ENTITY.id]: 0, }; - const counts: Record = {}; - for (const entity of preorder(rootEntity, (e) => e.children ?? [])) { - const { count } = await subsettingClient - .getEntityCount(id, entity.id, debouncedFilters ?? []) - .catch((error) => { - console.warn( - 'Could not load count for entity', - entity.id, - entity.displayName - ); - console.error(error); - return { count: 0 }; - }); - counts[entity.id] = count; - } - return counts; - }, [rootEntity, subsettingClient, id, debouncedFilters]) + const countsEntries = await Promise.all( + entities.map((entity) => + subsettingClient + .getEntityCount(id, entity.id, debouncedFilters ?? []) + .then( + ({ count }) => [entity.id, count], + (error) => { + console.warn( + 'Could not load count for entity', + entity.id, + entity.displayName + ); + console.error(error); + return [entity.id, 0]; + } + ) + ) + ); + return Object.fromEntries(countsEntries); + }, [rootEntity, entities, subsettingClient, id, debouncedFilters]) ); } diff --git a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx index e3efc18559..a26fe02cb1 100644 --- a/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx +++ b/packages/libs/eda/src/lib/map/analysis/MapAnalysis.tsx @@ -135,7 +135,7 @@ interface Props { } export function MapAnalysis(props: Props) { - const analysisState = useAnalysis(props.analysisId, 'pass-through'); + const analysisState = useAnalysis(props.analysisId, 'pass'); const appStateAndSetters = useAppState('@@mapApp@@', analysisState); if (appStateAndSetters.appState == null) return null; return ( @@ -959,8 +959,8 @@ function MapAnalysisImpl(props: Props & CompleteAppState) { {legendItems.length > 0 && ( diff --git a/packages/libs/eda/src/lib/workspace/Subsetting/SubsetDownloadModal.tsx b/packages/libs/eda/src/lib/workspace/Subsetting/SubsetDownloadModal.tsx index 8943f8ab75..cb0964a3c9 100644 --- a/packages/libs/eda/src/lib/workspace/Subsetting/SubsetDownloadModal.tsx +++ b/packages/libs/eda/src/lib/workspace/Subsetting/SubsetDownloadModal.tsx @@ -435,7 +435,6 @@ export default function SubsetDownloadModal({ position: 'relative', flex: '0 1 auto', minHeight: 0, - '.css-1nxo5ei-HeaderCell': { height: 'auto' }, }} className="DataGrid-Wrapper" ref={dataGridWrapperRef} @@ -449,7 +448,6 @@ export default function SubsetDownloadModal({ headerCells: { textTransform: 'none', position: 'relative', - height: '100%', }, table: { width: '100%', @@ -750,10 +748,8 @@ export default function SubsetDownloadModal({ } }; - const { - observe: observeModalHeader, - width: modalHeaderWidth, - } = useDimensions(); + const { observe: observeModalHeader, width: modalHeaderWidth } = + useDimensions(); // ~18px (round to 20px) per character for medium title size const maxStudyNameLength = Math.floor(modalHeaderWidth / 20); diff --git a/packages/libs/wdk-client/src/Views/Records/RecordNavigation/RecordNavigationItem.jsx b/packages/libs/wdk-client/src/Views/Records/RecordNavigation/RecordNavigationItem.jsx index e84f24982e..38cfbd8e7e 100644 --- a/packages/libs/wdk-client/src/Views/Records/RecordNavigation/RecordNavigationItem.jsx +++ b/packages/libs/wdk-client/src/Views/Records/RecordNavigation/RecordNavigationItem.jsx @@ -27,7 +27,6 @@ let RecordNavigationItem = ({
      diff --git a/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableAddColumnsDialog.tsx b/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableAddColumnsDialog.tsx index 9841b96596..27e23b93cb 100644 --- a/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableAddColumnsDialog.tsx +++ b/packages/libs/wdk-client/src/Views/ResultTableSummaryView/ResultTableAddColumnsDialog.tsx @@ -188,6 +188,14 @@ function ResultTableAddColumnsDialog({ padding: 0, }, }, + treeNode: { + nodeWrapper: { + padding: '2px 0', + }, + topLevelNodeWrapper: { + padding: '2px 0', + }, + }, }} /> {buttonWithTooltip} diff --git a/packages/libs/web-common/src/components/homepage/SearchPane.tsx b/packages/libs/web-common/src/components/homepage/SearchPane.tsx index 04ec7b3e02..cf2e5a7c2e 100644 --- a/packages/libs/web-common/src/components/homepage/SearchPane.tsx +++ b/packages/libs/web-common/src/components/homepage/SearchPane.tsx @@ -268,14 +268,20 @@ function SearchPaneNode({
      - - - + + + +
      {displayName}
      diff --git a/packages/libs/web-common/src/controllers/EdaMapController.tsx b/packages/libs/web-common/src/controllers/EdaMapController.tsx index 8640800d3c..27d6d42dee 100644 --- a/packages/libs/web-common/src/controllers/EdaMapController.tsx +++ b/packages/libs/web-common/src/controllers/EdaMapController.tsx @@ -20,7 +20,10 @@ export function EdaMapController() { siteInformationProps={{ loginUrl: '/user/login', siteHomeUrl: webAppUrl, - siteLogoSrc: `${webAppUrl}/images/VEuPathDB/icons-footer/${projectId.toLowerCase()}.png`, + // TODO Remove hardcoded logo after demo + // Hardcode veupathdb logo for now. + // siteLogoSrc: `${webAppUrl}/images/VEuPathDB/icons-footer/${projectId.toLowerCase()}.png`, + siteLogoSrc: `${webAppUrl}/images/VEuPathDB/icons-footer/VEuPathDB.png`, siteName: projectConfig?.displayName ?? '', }} sharingUrl={''} diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx index bce98e85c1..e2a049dcf1 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/homepage/VEuPathDBHomePage.tsx @@ -350,44 +350,48 @@ const useHeaderMenuItems = ( const alphabetizedSearchTree = useAlphabetizedSearchTree(searchTree); const communitySite = useCommunitySiteRootUrl(); - const mapMenuItems = useWdkService(async (wdkService): Promise< - HeaderMenuItem[] - > => { - try { - const anwser = await wdkService.getAnswerJson( - { - searchName: 'AllDatasets', - searchConfig: { - parameters: {}, + const showInteractiveMaps = Boolean(useEda && projectId === 'VectorBase'); + + const mapMenuItems = useWdkService( + async (wdkService): Promise => { + if (!showInteractiveMaps) return []; + try { + const anwser = await wdkService.getAnswerJson( + { + searchName: 'AllDatasets', + searchConfig: { + parameters: {}, + }, }, - }, - { - attributes: ['eda_study_id'], - } - ); - return anwser.records - .filter((record) => record.attributes.eda_study_id != null) - .map((record) => ({ - key: `map-${record.id[0].value}`, - display: record.displayName, - type: 'reactRoute', - url: `/maps/${record.id[0].value}/new`, - })); - } catch (error) { - console.error(error); - return [ - { - key: 'maps-error', - display: ( - <> - Could not load map data - - ), - type: 'custom', - }, - ]; - } - }); + { + attributes: ['eda_study_id'], + } + ); + return anwser.records + .filter((record) => record.attributes.eda_study_id != null) + .map((record) => ({ + key: `map-${record.id[0].value}`, + display: record.displayName, + type: 'reactRoute', + url: `/maps/${record.id[0].value}/new`, + })); + } catch (error) { + console.error(error); + return [ + { + key: 'maps-error', + display: ( + <> + Could not load map data + + ), + type: 'custom', + }, + ]; + } + }, + [showInteractiveMaps] + ); // type: reactRoute, webAppRoute, externalLink, subMenu, custom const fullMenuItemEntries: HeaderMenuItemEntry[] = [ @@ -593,14 +597,20 @@ const useHeaderMenuItems = ( key: 'maps-alpha', display: ( <> - Maps BETA + Interactive maps BETA ), type: 'subMenu', metadata: { - include: useEda ? [VectorBase] : [], + test: () => showInteractiveMaps, }, - items: mapMenuItems ?? [], + items: mapMenuItems ?? [ + { + key: 'maps-loading', + type: 'custom', + display: , + }, + ], }, { key: 'pubcrawler', diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx new file mode 100644 index 0000000000..5099634a84 --- /dev/null +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/AlphaFoldAttributeSection.tsx @@ -0,0 +1,32 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + BlockRecordAttributeSection, + Props, +} from '@veupathdb/wdk-client/lib/Views/Records/RecordAttributes/RecordAttributeSection'; + +/* + * This component does two things: + * + * 1. It imports the required assets needed to render + * the web component. + * 2. It renders the attribute section as a block section. + */ + +export function AlphaFoldRecordSection(props: Props) { + const areAssetsLoadingRef = useRef(false); + useEffect(() => { + if (!props.isCollapsed && !areAssetsLoadingRef.current) { + // Using dynamic import to lazy load these scripts + // @ts-ignore + import('../../../../../../vendored/pdbe-molstar-light-3.0.0.css'); + // @ts-ignore + import('../../../../../../vendored/pdbe-molstar-component-3.0.0.js'); + areAssetsLoadingRef.current = true; + } + }, [props.isCollapsed]); + return ( + <> + + + ); +} diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx index ecd9b6e394..088bf70144 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/components/records/GeneRecordClasses.GeneRecordClass.jsx @@ -5,180 +5,241 @@ import { connect } from 'react-redux'; import { RecordActions } from '@veupathdb/wdk-client/lib/Actions'; import * as Category from '@veupathdb/wdk-client/lib/Utils/CategoryUtils'; -import { CollapsibleSection, CategoriesCheckboxTree, Loading, RecordTable as WdkRecordTable } from '@veupathdb/wdk-client/lib/Components'; -import { renderAttributeValue, pure } from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; -import {Seq} from '@veupathdb/wdk-client/lib/Utils/IterableUtils'; -import {preorderSeq} from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; +import { + CollapsibleSection, + CategoriesCheckboxTree, + Loading, + RecordTable as WdkRecordTable, +} from '@veupathdb/wdk-client/lib/Components'; +import { + renderAttributeValue, + pure, +} from '@veupathdb/wdk-client/lib/Utils/ComponentUtils'; +import { Seq } from '@veupathdb/wdk-client/lib/Utils/IterableUtils'; +import { preorderSeq } from '@veupathdb/wdk-client/lib/Utils/TreeUtils'; import DatasetGraph from '@veupathdb/web-common/lib/components/DatasetGraph'; import { ExternalResourceContainer } from '@veupathdb/web-common/lib/components/ExternalResource'; import Sequence from '@veupathdb/web-common/lib/components/records/Sequence'; -import {findChildren, isNodeOverflowing} from '@veupathdb/web-common/lib/util/domUtils'; +import { + findChildren, + isNodeOverflowing, +} from '@veupathdb/web-common/lib/util/domUtils'; import { updateTableState } from '../../actioncreators/RecordViewActionCreators'; import { projectId, webAppUrl } from '../../config'; import * as Gbrowse from '../common/Gbrowse'; -import {OverviewThumbnails} from '../common/OverviewThumbnails'; -import {SnpsAlignmentForm} from '../common/Snps'; +import { OverviewThumbnails } from '../common/OverviewThumbnails'; +import { SnpsAlignmentForm } from '../common/Snps'; import { addCommentLink } from '../common/UserComments'; import { withRequestFields } from './utils'; -import { usePreferredOrganismsEnabledState, usePreferredOrganismsState } from '@veupathdb/preferred-organisms/lib/hooks/preferredOrganisms'; +import { + usePreferredOrganismsEnabledState, + usePreferredOrganismsState, +} from '@veupathdb/preferred-organisms/lib/hooks/preferredOrganisms'; import { BlockRecordAttributeSection } from '@veupathdb/wdk-client/lib/Views/Records/RecordAttributes/RecordAttributeSection'; import betaImage from '@veupathdb/wdk-client/lib/Core/Style/images/beta2-30.png'; import { LinksPosition } from '@veupathdb/coreui/dist/components/inputs/checkboxes/CheckboxTree/CheckboxTree'; +import { AlphaFoldRecordSection } from './AlphaFoldAttributeSection'; /** * Render thumbnails at eupathdb-GeneThumbnailsContainer */ export const RecordHeading = connect( - state => ({ categoryTree: state.record.categoryTree }), + (state) => ({ categoryTree: state.record.categoryTree }), RecordActions -)(class RecordHeading extends Component { - - constructor(...args) { - super(...args); - this.handleThumbnailClick = this.handleThumbnailClick.bind(this); - this.addProductTooltip = lodash.debounce(this.addProductTooltip.bind(this), 300); - } - - componentDidMount() { - this.addProductTooltip(); - window.addEventListener('resize', this.addProductTooltip); - this.thumbsContainer = this.node.querySelector('.eupathdb-ThumbnailsContainer'); - if (this.thumbsContainer) this.renderThumbnails(); - else console.error('Warning: Could not find ThumbnailsContainer'); - } - - handleThumbnailClick({ anchor }) { - this.props.updateSectionVisibility(anchor, true); - } - - componentDidUpdate() { - if (this.thumbsContainer) this.renderThumbnails(); - } +)( + class RecordHeading extends Component { + constructor(...args) { + super(...args); + this.handleThumbnailClick = this.handleThumbnailClick.bind(this); + this.addProductTooltip = lodash.debounce( + this.addProductTooltip.bind(this), + 300 + ); + } - componentWillUnmount() { - if (this.thumbsContainer) ReactDOM.unmountComponentAtNode(this.thumbsContainer); - window.removeEventListener('resize', this.addProductTooltip); - this.addProductTooltip.cancel(); - } + componentDidMount() { + this.addProductTooltip(); + window.addEventListener('resize', this.addProductTooltip); + this.thumbsContainer = this.node.querySelector( + '.eupathdb-ThumbnailsContainer' + ); + if (this.thumbsContainer) this.renderThumbnails(); + else console.error('Warning: Could not find ThumbnailsContainer'); + } - addProductTooltip() { - let products = Seq.from( - this.node.querySelectorAll( - '.eupathdb-RecordOverviewTitle, .eupathdb-GeneOverviewSubtitle')) - .filter(isNodeOverflowing) - .flatMap(findChildren('.eupathdb-RecordOverviewDescription')); + handleThumbnailClick({ anchor }) { + this.props.updateSectionVisibility(anchor, true); + } - let items = Seq.from( - this.node.querySelectorAll('.eupathdb-RecordOverviewItem')) - .filter(isNodeOverflowing); + componentDidUpdate() { + if (this.thumbsContainer) this.renderThumbnails(); + } - products.concat(items) - .forEach(target => { target.title = target.textContent }); - } + componentWillUnmount() { + if (this.thumbsContainer) + ReactDOM.unmountComponentAtNode(this.thumbsContainer); + window.removeEventListener('resize', this.addProductTooltip); + this.addProductTooltip.cancel(); + } - renderThumbnails() { - let { categoryTree, recordClass } = this.props; - let { attributes, tables } = this.props.record; - // Get field present in record instance. This is leveraging the fact that - // we filter the category tree in the store based on the contents of - // MetaTable. - let instanceFields = new Set( - preorderSeq(categoryTree) - .filter(node => !node.children.length) - .map(node => node.properties.name[0])); - - let transcriptomicsThumbnail = { - displayName: 'Transcriptomics', - element: , - anchor: 'TranscriptionSummary' - }; + addProductTooltip() { + let products = Seq.from( + this.node.querySelectorAll( + '.eupathdb-RecordOverviewTitle, .eupathdb-GeneOverviewSubtitle' + ) + ) + .filter(isNodeOverflowing) + .flatMap(findChildren('.eupathdb-RecordOverviewDescription')); + + let items = Seq.from( + this.node.querySelectorAll('.eupathdb-RecordOverviewItem') + ).filter(isNodeOverflowing); + + products.concat(items).forEach((target) => { + target.title = target.textContent; + }); + } - let phenotypeThumbnail = { - displayName: 'Phenotype', - element: , - anchor: 'PhenotypeGraphs' - }; + renderThumbnails() { + let { categoryTree, recordClass } = this.props; + let { attributes, tables } = this.props.record; + // Get field present in record instance. This is leveraging the fact that + // we filter the category tree in the store based on the contents of + // MetaTable. + let instanceFields = new Set( + preorderSeq(categoryTree) + .filter((node) => !node.children.length) + .map((node) => node.properties.name[0]) + ); - let crisprPhenotypeThumbnail = { - displayName: 'Phenotype', - element: , - anchor: 'CrisprPhenotypeGraphs' - }; + let transcriptomicsThumbnail = { + displayName: 'Transcriptomics', + element: ( + + ), + anchor: 'TranscriptionSummary', + }; + + let phenotypeThumbnail = { + displayName: 'Phenotype', + element: ( + + ), + anchor: 'PhenotypeGraphs', + }; + + let crisprPhenotypeThumbnail = { + displayName: 'Phenotype', + element: ( + + ), + anchor: 'CrisprPhenotypeGraphs', + }; + + let filteredGBrowseContexts = Seq.from(Gbrowse.contexts) + .filter((context) => context.includeInThumbnails !== false) + // inject transcriptomicsThumbnail before protein thumbnails + .flatMap((context) => { + if (context.gbrowse_url === 'SnpsGbrowseUrl') { + return [phenotypeThumbnail, crisprPhenotypeThumbnail, context]; + } + if (context.gbrowse_url === 'FeaturesPbrowseUrl') { + return [transcriptomicsThumbnail, context]; + } + return [context]; + }) + // remove thumbnails whose associated fields are not present in record instance + .filter((context) => instanceFields.has(context.anchor)) + .map((context) => + context === transcriptomicsThumbnail || + context === phenotypeThumbnail || + context === crisprPhenotypeThumbnail + ? Object.assign({}, context, { + data: { + count: + tables && + tables[context.anchor] && + tables[context.anchor].length, + }, + }) + : Object.assign({}, context, { + element: ( + + ), + displayName: + recordClass.attributesMap[context.gbrowse_url].displayName, + }) + ) + .toArray(); + + ReactDOM.render( + , + this.thumbsContainer + ); + } - let filteredGBrowseContexts = Seq.from(Gbrowse.contexts) - .filter(context => context.includeInThumbnails !== false) - // inject transcriptomicsThumbnail before protein thumbnails - .flatMap(context => { - if (context.gbrowse_url === 'SnpsGbrowseUrl') { - return [ phenotypeThumbnail, crisprPhenotypeThumbnail, context ]; - } - if (context.gbrowse_url === 'FeaturesPbrowseUrl') { - return [ transcriptomicsThumbnail, context ]; - } - return [ context ]; - }) - // remove thumbnails whose associated fields are not present in record instance - .filter(context => instanceFields.has(context.anchor)) - .map(context => context === transcriptomicsThumbnail - || context === phenotypeThumbnail - || context === crisprPhenotypeThumbnail - ? Object.assign({}, context, { - data: { - count: tables && tables[context.anchor] && tables[context.anchor].length - } - }) - : Object.assign({}, context, { - element: , - displayName: recordClass.attributesMap[context.gbrowse_url].displayName - }) - ) - .toArray(); - - ReactDOM.render(( - - ), this.thumbsContainer); + render() { + // FungiVBOrgLinkoutsTable is requested in componentWrappers + return ( + +
      (this.node = node)}> + +
      + +
      + ); + } } +); - render() { - // FungiVBOrgLinkoutsTable is requested in componentWrappers +export const RecordMainSection = connect(null)( + ({ DefaultComponent, dispatch, ...props }) => { return ( -
      this.node = node}> - -
      - + {props.depth == null && ( +
      + +   + +
      + )} +
      ); } - -}); - -export const RecordMainSection = connect(null)(({ DefaultComponent, dispatch, ...props }) => { - return ( - - {props.depth == null && ( -
      -   - -
      - )} - -
      - ); - }); +); export function RecordAttributeSection(props) { const { DefaultComponent, ...restProps } = props; - switch(restProps.attribute.name) { + switch (restProps.attribute.name) { case 'alphafold_url': - return ; + return ; default: return ; } @@ -188,175 +249,245 @@ function FungiVBOrgLinkoutsTable(props) { if (props.value == null || props.value.length === 0) return null; const groupedLinks = lodash.groupBy(props.value, 'dataset'); return ( -
      -
      Model Organism Database(s)
      - {Object.entries(groupedLinks).map(([dataset, rows]) => -
      - {dataset}: {rows.map((row, index) => +
      +
      + Model Organism Database(s) +
      + {Object.entries(groupedLinks).map(([dataset, rows]) => ( +
      + {dataset}:{' '} + {rows.map((row, index) => ( {renderAttributeValue(row.link)} {index === rows.length - 1 ? null : ', '} - )} + ))}
      - )} + ))}
      ); } -const ExpressionChildRow = makeDatasetGraphChildRow('ExpressionGraphsDataTable'); -const HostResponseChildRow = makeDatasetGraphChildRow('HostResponseGraphsDataTable', 'FacetMetadata', 'ContXAxisMetadata'); -const CrisprPhenotypeChildRow = makeDatasetGraphChildRow('CrisprPhenotypeGraphsDataTable'); -const PhenotypeScoreChildRow = makeDatasetGraphChildRow('PhenotypeScoreGraphsDataTable'); +const ExpressionChildRow = makeDatasetGraphChildRow( + 'ExpressionGraphsDataTable' +); +const HostResponseChildRow = makeDatasetGraphChildRow( + 'HostResponseGraphsDataTable', + 'FacetMetadata', + 'ContXAxisMetadata' +); +const CrisprPhenotypeChildRow = makeDatasetGraphChildRow( + 'CrisprPhenotypeGraphsDataTable' +); +const PhenotypeScoreChildRow = makeDatasetGraphChildRow( + 'PhenotypeScoreGraphsDataTable' +); const PhenotypeChildRow = makeDatasetGraphChildRow('PhenotypeGraphsDataTable'); -const UDTranscriptomicsChildRow = makeDatasetGraphChildRow('UserDatasetsTranscriptomicsGraphsDataTable'); +const UDTranscriptomicsChildRow = makeDatasetGraphChildRow( + 'UserDatasetsTranscriptomicsGraphsDataTable' +); export function RecordTable(props) { - switch(props.table.name) { - + switch (props.table.name) { case 'ExpressionGraphs': case 'ProteinExpressionGraphs': case 'eQTLPhenotypeGraphs': - return + return ( + + ); case 'GOTerms': - return + return ; case 'HostResponseGraphs': - return + return ( + + ); case 'CrisprPhenotypeGraphs': - return + return ( + + ); case 'PhenotypeScoreGraphs': - return + return ( + + ); case 'PhenotypeGraphs': - return + return ; case 'UserDatasetsTranscriptomicsGraphs': - return + return ( + + ); case 'MercatorTable': - return + return ; case 'Orthologs': return ( }> - + ); case 'WolfPsortForm': - return + return ; case 'BlastpForm': - return + return ; case 'MitoprotForm': - return + return ; case 'InterProForm': - return + return ; case 'MendelGPIForm': - return + return ; case 'StringDBForm': - return + return ; case 'ProteinProperties': - return + return ( + + ); case 'ProteinExpressionPBrowse': - return + return ( + + ); case 'Sequences': - return + return ( + + ); case 'UserComments': - return + return ; case 'SNPsAlignment': - return + return ; case 'RodMalPhenotype': - return + return ( + + ); case 'TranscriptionSummary': - return + return ; case 'Cellxgene': - return - + return ( + + ); default: - return + return ; } } /** Customize how a record table's description is rendered **/ export function RecordTableDescription(props) { - switch(props.table.name) { - + switch (props.table.name) { /* Example: Render the content of the attribute `orthomdl_link` in a `p` tag. case 'GeneTranscripts': return renderAttributeValue(props.record.attributes.orthomcl_link, null, 'p'); */ case 'ECNumbers': - return typeof props.record.tables.ECNumbers != "undefined" && props.record.tables.ECNumbers.length > 0 && renderAttributeValue(props.record.attributes.ec_number_warning, null, 'p'); + return ( + typeof props.record.tables.ECNumbers != 'undefined' && + props.record.tables.ECNumbers.length > 0 && + renderAttributeValue( + props.record.attributes.ec_number_warning, + null, + 'p' + ) + ); case 'ECNumbersInferred': - return typeof props.record.tables.ECNumbersInferred != "undefined" && props.record.tables.ECNumbersInferred.length > 0 && renderAttributeValue(props.record.attributes.ec_inferred_description, null, 'p'); + return ( + typeof props.record.tables.ECNumbersInferred != 'undefined' && + props.record.tables.ECNumbersInferred.length > 0 && + renderAttributeValue( + props.record.attributes.ec_inferred_description, + null, + 'p' + ) + ); case 'MetabolicPathways': - return typeof props.record.tables.MetabolicPathways != "undefined" && props.record.tables.MetabolicPathways.length > 0 && renderAttributeValue(props.record.attributes.ec_num_warn, null, 'p'); + return ( + typeof props.record.tables.MetabolicPathways != 'undefined' && + props.record.tables.MetabolicPathways.length > 0 && + renderAttributeValue(props.record.attributes.ec_num_warn, null, 'p') + ); case 'CompoundsMetabolicPathways': - return typeof props.record.tables.CompoundsMetabolicPathways != "undefined" && props.record.tables.CompoundsMetabolicPathways.length > 0 && renderAttributeValue(props.record.attributes.ec_num_warn, null, 'p'); + return ( + typeof props.record.tables.CompoundsMetabolicPathways != 'undefined' && + props.record.tables.CompoundsMetabolicPathways.length > 0 && + renderAttributeValue(props.record.attributes.ec_num_warn, null, 'p') + ); case 'AlphaFoldLinkouts': - return typeof props.record.tables.AlphaFoldLinkouts != "undefined" && props.record.tables.AlphaFoldLinkouts.length > 0 && renderAttributeValue(props.record.attributes.alphafold_table_help, null, 'p'); - + return ( + typeof props.record.tables.AlphaFoldLinkouts != 'undefined' && + props.record.tables.AlphaFoldLinkouts.length > 0 && + renderAttributeValue( + props.record.attributes.alphafold_table_help, + null, + 'p' + ) + ); case 'LOPITResult': - return typeof props.record.tables.LOPITResult != "undefined" && props.record.tables.LOPITResult.length > 0 && renderAttributeValue(props.record.attributes.LOPITGraphSVG, null, 'p'); - + return ( + typeof props.record.tables.LOPITResult != 'undefined' && + props.record.tables.LOPITResult.length > 0 && + renderAttributeValue(props.record.attributes.LOPITGraphSVG, null, 'p') + ); default: - return + return ; } } function SNPsAlignment(props) { - let { start_min, end_max, sequence_id, organism_full } = props.record.attributes; + let { start_min, end_max, sequence_id, organism_full } = + props.record.attributes; return ( - ) + organism={organism_full} + /> + ); } -const RodMalPhenotypeTableChildRow = pure(function RodMalPhenotypeTableChildRow(props) { - let { - phenotype - } = props.rowData; +const RodMalPhenotypeTableChildRow = pure(function RodMalPhenotypeTableChildRow( + props +) { + let { phenotype } = props.rowData; return (
      - Phenotype: - {phenotype == null ? null : phenotype} + Phenotype:{phenotype == null ? null : phenotype}
      - ) + ); }); - const CellxgeneTableChildRow = pure(function CellxgeneTableChildRow(props) { - let { - source_id,source_ids,dataset_name,project_id - } = props.rowData; + let { source_id, source_ids, dataset_name, project_id } = props.rowData; return ( - ) + /> + ); }); - -function makeDatasetGraphChildRow(dataTableName, facetMetadataTableName, contXAxisMetadataTableName) { +function makeDatasetGraphChildRow( + dataTableName, + facetMetadataTableName, + contXAxisMetadataTableName +) { let DefaultComponent = WdkRecordTable; - return connect(state => { + return connect((state) => { let { record, recordClass } = state.record; - let dataTable = dataTableName && dataTableName in record.tables && { - value: record.tables[dataTableName], - table: recordClass.tablesMap[dataTableName], - record: record, - recordClass: recordClass, - DefaultComponent: DefaultComponent - }; - - let facetMetadataTable = facetMetadataTableName && facetMetadataTableName in record.tables && { - value: record.tables[facetMetadataTableName], - table: recordClass.tablesMap[facetMetadataTableName], - record: record, - recordClass: recordClass, - DefaultComponent: DefaultComponent - }; - - let contXAxisMetadataTable = contXAxisMetadataTableName && contXAxisMetadataTableName in record.tables && { - value: record.tables[contXAxisMetadataTableName], - table: recordClass.tablesMap[contXAxisMetadataTableName], - record: record, - recordClass: recordClass, - DefaultComponent: DefaultComponent - }; + let dataTable = dataTableName && + dataTableName in record.tables && { + value: record.tables[dataTableName], + table: recordClass.tablesMap[dataTableName], + record: record, + recordClass: recordClass, + DefaultComponent: DefaultComponent, + }; + + let facetMetadataTable = facetMetadataTableName && + facetMetadataTableName in record.tables && { + value: record.tables[facetMetadataTableName], + table: recordClass.tablesMap[facetMetadataTableName], + record: record, + recordClass: recordClass, + DefaultComponent: DefaultComponent, + }; + + let contXAxisMetadataTable = contXAxisMetadataTableName && + contXAxisMetadataTableName in record.tables && { + value: record.tables[contXAxisMetadataTableName], + table: recordClass.tablesMap[contXAxisMetadataTableName], + record: record, + recordClass: recordClass, + DefaultComponent: DefaultComponent, + }; return { dataTable, facetMetadataTable, contXAxisMetadataTable }; })(withRequestFields(Wrapper)); @@ -407,11 +544,11 @@ function makeDatasetGraphChildRow(dataTableName, facetMetadataTableName, contXAx tables: [ dataTableName, facetMetadataTableName, - contXAxisMetadataTableName - ].filter(tableName => tableName != null) - }) + contXAxisMetadataTableName, + ].filter((tableName) => tableName != null), + }); }, []); - return ; + return ; } } @@ -429,29 +566,31 @@ function makeGenomicRegions( return allGenomicRegions; } - const { genomicRegions } = allGenomicRegions.reverse().reduce(function (memo, region) { - const [ regionType, regionStart, regionEnd ] = region; + const { genomicRegions } = allGenomicRegions.reverse().reduce( + function (memo, region) { + const [regionType, regionStart, regionEnd] = region; - if ( - memo.omissionLength < threePrimeUtrLength && - regionType === 'UTR' - ) { - memo.omissionLength += regionEnd - regionStart + 1; - } else { - memo.genomicRegions.push(region); - } + if (memo.omissionLength < threePrimeUtrLength && regionType === 'UTR') { + memo.omissionLength += regionEnd - regionStart + 1; + } else { + memo.genomicRegions.push(region); + } - return memo; - }, { genomicRegions: [], omissionLength: 0 }); + return memo; + }, + { genomicRegions: [], omissionLength: 0 } + ); return genomicRegions.reverse(); } -const renderUtr = str => +const renderUtr = (str) => ( {str.toLowerCase()} +); -const renderIntron = str => +const renderIntron = (str) => ( {str.toLowerCase()} +); const SequencesTableChildRow = pure(function SequencesTableChildRow(props) { let { @@ -466,40 +605,41 @@ const SequencesTableChildRow = pure(function SequencesTableChildRow(props) { five_prime_utr_coords, three_prime_utr_coords, gen_rel_intron_utr_coords, - transcript_type + transcript_type, } = props.rowData; let shouldOmitThreePrimeUtr = transcript_type === 'pseudogenic_transcript'; let threePrimeUtrCoords = useMemo( () => JSON.parse(three_prime_utr_coords), - [ three_prime_utr_coords ] + [three_prime_utr_coords] ); let transcriptRegions = [ JSON.parse(five_prime_utr_coords) || undefined, - (!shouldOmitThreePrimeUtr && threePrimeUtrCoords) || undefined - ].filter(coords => coords != null) - let transcriptHighlightRegions = transcriptRegions.map(coords => { + (!shouldOmitThreePrimeUtr && threePrimeUtrCoords) || undefined, + ].filter((coords) => coords != null); + let transcriptHighlightRegions = transcriptRegions.map((coords) => { return { renderRegion: renderUtr, start: coords[0], end: coords[1] }; }); let genomicRegions = useMemo( - () => makeGenomicRegions( - gen_rel_intron_utr_coords, - shouldOmitThreePrimeUtr, - !threePrimeUtrCoords - ? -Infinity - : threePrimeUtrCoords[1] - threePrimeUtrCoords[0] + 1 - ), - [ gen_rel_intron_utr_coords, shouldOmitThreePrimeUtr, threePrimeUtrCoords ] + () => + makeGenomicRegions( + gen_rel_intron_utr_coords, + shouldOmitThreePrimeUtr, + !threePrimeUtrCoords + ? -Infinity + : threePrimeUtrCoords[1] - threePrimeUtrCoords[0] + 1 + ), + [gen_rel_intron_utr_coords, shouldOmitThreePrimeUtr, threePrimeUtrCoords] ); - let genomicHighlightRegions = genomicRegions.map(coord => { + let genomicHighlightRegions = genomicRegions.map((coord) => { return { renderRegion: coord[0] === 'Intron' ? renderIntron : renderUtr, start: coord[1], - end: coord[2] + end: coord[2], }; }); let genomicRegionTypes = lodash(genomicRegions) - .map(region => region[0]) + .map((region) => region[0]) .sortBy() .sortedUniq() .value(); @@ -510,78 +650,84 @@ const SequencesTableChildRow = pure(function SequencesTableChildRow(props) { {protein_sequence == null ? null : (

      Predicted Protein Sequence

      -
      {protein_length} aa
      - -
      - )} - {protein_sequence == null ? null :
      } -
      -

      Predicted RNA/mRNA Sequence (Introns spliced out{ transcriptRegions.length > 0 ? '; UTRs highlighted' : null })

      - {transcript_length} bp - { transcriptRegions.length > 0 - ? {renderUtr('UTR')} - : null } + {protein_length} aa
      - +
      -
      -

      Genomic Sequence { genomicRegionTypes.length > 0 ? ' (' + genomicRegionTypes.map(t => t + 's').join(' and ') + ' highlighted)' : null}

      -
      - {genomic_sequence_length} bp - {genomicRegionTypes.map(t => { - const renderStr = t === 'Intron' ? renderIntron : renderUtr; - return ( - {renderStr(t)} - ); - })} -
      - + )} + {protein_sequence == null ? null :
      } +
      +

      + Predicted RNA/mRNA Sequence (Introns spliced out + {transcriptRegions.length > 0 ? '; UTRs highlighted' : null}) +

      +
      + {transcript_length} bp + {transcriptRegions.length > 0 ? ( + {renderUtr('UTR')} + ) : null} +
      + +
      +
      +

      + Genomic Sequence{' '} + {genomicRegionTypes.length > 0 + ? ' (' + + genomicRegionTypes.map((t) => t + 's').join(' and ') + + ' highlighted)' + : null} +

      +
      + {genomic_sequence_length} bp + {genomicRegionTypes.map((t) => { + const renderStr = t === 'Intron' ? renderIntron : renderUtr; + return {renderStr(t)}; + })}
      +
      +
      ); }); -function makeTree(rows){ - const n = Category.createNode; // helper for below - let myTree = n('root', 'root', null, []); - addChildren(myTree, rows, n); - return myTree; +function makeTree(rows) { + const n = Category.createNode; // helper for below + let myTree = n('root', 'root', null, []); + addChildren(myTree, rows, n); + return myTree; } function addChildren(t, rows, n) { - for(let i = 0; i < rows.length; i++){ - let parent = rows[i].parent; - let organism = rows[i].organism; - let abbrev = rows[i].abbrev; - if(parent == Category.getId(t) ){ - let node = n(abbrev, organism, null, []); - t.children.push(node); - } - } - for(let j = 0; j < t.children.length; j++) { - addChildren(t.children[j], rows, n); + for (let i = 0; i < rows.length; i++) { + let parent = rows[i].parent; + let organism = rows[i].organism; + let abbrev = rows[i].abbrev; + if (parent == Category.getId(t)) { + let node = n(abbrev, organism, null, []); + t.children.push(node); } + } + for (let j = 0; j < t.children.length; j++) { + addChildren(t.children[j], rows, n); + } } - class MercatorTable extends React.Component { constructor(props) { super(props); this.state = { selectedLeaves: [], - expandedBranches: [] + expandedBranches: [], }; this.handleChange = this.handleChange.bind(this); this.handleUiChange = this.handleUiChange.bind(this); @@ -589,26 +735,37 @@ class MercatorTable extends React.Component { this.handleSearchTermChange = this.handleSearchTermChange.bind(this); } handleChange(selectedLeaves) { - this.setState({selectedLeaves}); + this.setState({ selectedLeaves }); } handleUiChange(expandedBranches) { - this.setState({expandedBranches}); + this.setState({ expandedBranches }); } handleSubmit() { - this.props.onChange(this.props.isMultiPick ? this.state.selectedLeaves : this.state.selectedLeaves[0]); + this.props.onChange( + this.props.isMultiPick + ? this.state.selectedLeaves + : this.state.selectedLeaves[0] + ); } handleSearchTermChange(searchTerm) { - this.setState({searchTerm}); + this.setState({ searchTerm }); } render() { let exceededMaxOrganisms = this.state.selectedLeaves.length > 15; return (
      - +
      - +
      @@ -622,7 +779,10 @@ class MercatorTable extends React.Component { size="10" /> - - +
      -
      - - Organisms to align: - -

      - Select 15 or fewer organisms from the tree below. -
      - {exceededMaxOrganisms && } - - You have currently selected {this.state.selectedLeaves.length} - -

      - - -
      +
      + Organisms to align: + +

      + Select 15 or fewer organisms from the tree below. +
      + {exceededMaxOrganisms && ( + + )} + + You have currently selected {this.state.selectedLeaves.length} + +

      + + +
      -
      - Select output: -
      -
      -
      +
      + Select output: +
      + +
      +
      + +
      +
      - - -
      + + +
      ); } } - class SortKeyTable extends React.Component { - constructor(props) { super(props); // Memoize the sorting. Without this, the DataTable widget will think is // is a new table and reset the sorting. This is bad if a user has already // sorted the table. - this.sortValue = lodash.memoize(value => lodash.sortBy(value, 'sort_key')); + this.sortValue = lodash.memoize((value) => + lodash.sortBy(value, 'sort_key') + ); } render() { - return + return ( + + ); } } - class WolfPsortForm extends React.Component { - inputHeader(t) { - if(t.length > 1) { - return

      Select the Protein:

      - } + inputHeader(t) { + if (t.length > 1) { + return

      Select the Protein:

      ; } - printInputs(t) { - if(t.length == 1) { - return (); - } - return ( - t.map(p => { - return ( - - ); - }) - ); + } + printInputs(t) { + if (t.length == 1) { + return ( + + ); } + return t.map((p) => { + return ( + + ); + }); + } - render() { - let { project_id } = this.props.record.attributes; - let t = this.props.value; - return ( - -
      -
      - - - - - {this.inputHeader(t)} - {this.printInputs(t)} - -

      Select an organism type:

      - Animal
      - Plant
      - Fungi

      - -
      -
      - - ); - } + render() { + let { project_id } = this.props.record.attributes; + let t = this.props.value; + return ( +
      +
      + + + + {this.inputHeader(t)} + {this.printInputs(t)} +

      Select an organism type:

      + Animal +
      + Plant +
      + Fungi +
      +
      + +
      +
      + ); + } } - class BlastpForm extends React.Component { - - inputHeader(t) { - if(t.length > 1) { - return

      Select the Protein:

      - } + inputHeader(t) { + if (t.length > 1) { + return

      Select the Protein:

      ; } + } - printInputs(t) { - if(t.length == 1) { - return (); - } - - return ( - t.map(p => { - return ( - - ); - }) - ); + printInputs(t) { + if (t.length == 1) { + return ( + + ); } - render() { - let { project_id } = this.props.record.attributes; - let t = this.props.value; - + return t.map((p) => { return ( -
      -
      - - - - - {this.inputHeader(t)} - {this.printInputs(t)} - -

      Select the Database:

      - Non-redundant protein sequences (nr)
      - Reference proteins (refseq_protein)
      - UniProtKB/Swiss-Prot(swissprot)
      - Model Organisms (landmark)
      - Patented protein sequences(pat)
      - Protein Data Bank proteins(pdb)
      - Metagenomic proteins(env_nr)
      - Transcriptome Shotgun Assembly proteins (tsa_nr)

      - - -
      -
      - ); - } -} + + ); + }); + } + render() { + let { project_id } = this.props.record.attributes; + let t = this.props.value; -class MitoprotForm extends React.Component { + return ( +
      +
      + + + + {this.inputHeader(t)} + {this.printInputs(t)} +

      Select the Database:

      + Non-redundant + protein sequences (nr) +
      + {' '} + Reference proteins (refseq_protein) +
      + {' '} + UniProtKB/Swiss-Prot(swissprot) +
      + {' '} + Model Organisms (landmark) +
      + Patented protein + sequences(pat) +
      + Protein Data Bank + proteins(pdb) +
      + Metagenomic + proteins(env_nr) +
      + Transcriptome + Shotgun Assembly proteins (tsa_nr) +
      +
      + +
      +
      + ); + } +} - inputHeader(t) { - if(t.length > 1) { - return

      Select the Protein:

      - } +class MitoprotForm extends React.Component { + inputHeader(t) { + if (t.length > 1) { + return

      Select the Protein:

      ; } + } - printInputs(t) { - if(t.length == 1) { - return (); - } - - return ( - t.map(p => { - return ( - - ); - }) - ); + printInputs(t) { + if (t.length == 1) { + return ( + + ); } - render() { - let { project_id } = this.props.record.attributes; - let t = this.props.value; + return t.map((p) => { return ( -
      -
      - - + + ); + }); + } - {this.inputHeader(t)} - {this.printInputs(t)} + render() { + let { project_id } = this.props.record.attributes; + let t = this.props.value; + return ( +
      + + + - - -
      - ); - } -} + {this.inputHeader(t)} + {this.printInputs(t)} + + +
      + ); + } +} class InterProForm extends React.Component { - - inputHeader(t) { - if(t.length > 1) { - return

      Select the Protein:

      - } + inputHeader(t) { + if (t.length > 1) { + return

      Select the Protein:

      ; } + } - printInputs(t) { - if(t.length == 1) { - return (); - } - - return ( - t.map(p => { - return ( - - ); - }) - ); + printInputs(t) { + if (t.length == 1) { + return ( + + ); } - render() { - let { project_id } = this.props.record.attributes; - let t = this.props.value; + return t.map((p) => { return ( + + ); + }); + } -
      + render() { + let { project_id } = this.props.record.attributes; + let t = this.props.value; + return ( +
      - - - + + + - {this.inputHeader(t)} - {this.printInputs(t)} + {this.inputHeader(t)} + {this.printInputs(t)} - +
      - ); - } + ); + } } - class MendelGPIForm extends React.Component { - - inputHeader(t) { - if(t.length > 1) { - return

      Select the Protein:

      - } + inputHeader(t) { + if (t.length > 1) { + return

      Select the Protein:

      ; } + } - printInputs(t) { - if(t.length == 1) { - return (); - } - - return ( - t.map(p => { - return ( - - ); - }) - ); + printInputs(t) { + if (t.length == 1) { + return ( + + ); } - - render() { - let { project_id } = this.props.record.attributes; - let t = this.props.value; + return t.map((p) => { return ( -
      -
      - - - - {this.inputHeader(t)} - {this.printInputs(t)} - -

      Select Taxonomic Set:

      - Metazoa
      - Protozoa

      - -
      - -
      + ); - } -} + }); + } + render() { + let { project_id } = this.props.record.attributes; + let t = this.props.value; + return ( +
      +
      + + + {this.inputHeader(t)} + {this.printInputs(t)} +

      Select Taxonomic Set:

      + Metazoa +
      + Protozoa +
      +
      + +
      +
      + ); + } +} class StringDBForm extends React.Component { - - inputHeader(t) { - if(t.length > 1) { - return

      Select the Protein:

      - } - } - - printInputs(t) { - if(t.length == 1) { - return (); - } - - return ( - t.map(p => { - return ( - - ); - }) - ); + inputHeader(t) { + if (t.length > 1) { + return

      Select the Protein:

      ; } + } - printOrganismInputs(s,genus_species) { - const defaultOrganismEntry = s.find(p => p[1] === genus_species) || s[0]; + printInputs(t) { + if (t.length == 1) { return ( - + ); } - render() { - let {project_id, genus_species } = this.props.record.attributes; - let t = this.props.value; - let s = JSON.parse(t[0].jsonString); + return t.map((p) => { return ( -
      -
      - - - - {this.inputHeader(t)} - {this.printInputs(t)} - -

      Please select the organism:

      - {this.printOrganismInputs(s,genus_species)} -

      - -
      -
      - ); - } + + ); + }); + } + + printOrganismInputs(s, genus_species) { + const defaultOrganismEntry = s.find((p) => p[1] === genus_species) || s[0]; + return ( + + ); + } + + render() { + let { project_id, genus_species } = this.props.record.attributes; + let t = this.props.value; + let s = JSON.parse(t[0].jsonString); + return ( +
      +
      + + + + {this.inputHeader(t)} + {this.printInputs(t)} + +

      + Please select the organism: +
      +
      + {this.printOrganismInputs(s, genus_species)} +
      +

      + +
      +
      + ); + } } function OrthologsFormContainer(props) { - const [ preferredOrganismsEnabled ] = usePreferredOrganismsEnabledState(); + const [preferredOrganismsEnabled] = usePreferredOrganismsEnabledState(); - const [ preferredOrganisms ] = usePreferredOrganismsState(); + const [preferredOrganisms] = usePreferredOrganismsState(); const filteredValue = useMemo(() => { if (!preferredOrganismsEnabled) { @@ -1010,14 +1231,15 @@ function OrthologsFormContainer(props) { const preferredOrganismsSet = new Set(preferredOrganisms); - return props.value.filter(({ organism }) => preferredOrganismsSet.has(organism)); - }, [ props.value, preferredOrganisms, preferredOrganismsEnabled ]); + return props.value.filter(({ organism }) => + preferredOrganismsSet.has(organism) + ); + }, [props.value, preferredOrganisms, preferredOrganismsEnabled]); return ; } class OrthologsForm extends SortKeyTable { - toggleAll(checked) { const node = ReactDOM.findDOMNode(this); for (const input of node.querySelectorAll('input[name="gene_ids"]')) { @@ -1026,200 +1248,283 @@ class OrthologsForm extends SortKeyTable { } render() { - let { source_id, gene_type } = this.props.record.attributes; + let { source_id, gene_type } = this.props.record.attributes; - let is_protein = (gene_type === 'protein coding' || gene_type === 'protein coding gene') ? true : false; - let not_protein = is_protein ? false : true; + let is_protein = + gene_type === 'protein coding' || gene_type === 'protein coding gene' + ? true + : false; + let not_protein = is_protein ? false : true; - if ( (this.props.value.length === 0) || not_protein ) { - return ( ) - } else { - return ( -
      - - - - - - this.toggleAll(true)}/> - this.toggleAll(false)}/> -
      -

      Select sequence type for Clustal Omega multiple sequence alignment:

      -

      Please note: selecting a large flanking region or a large number of sequences will take several minutes to align.

      -
      - { is_protein && <> Protein } - { is_protein && <> CDS (spliced) } - Genomic - - nt upstream (max 2500) - nt downstream (max 2500) - -

      Output format:   - + + + + + this.toggleAll(true)} + /> + this.toggleAll(false)} + /> +
      +

      + + Select sequence type for Clustal Omega multiple sequence + alignment: + +

      +

      + Please note: selecting a large flanking region or a large number of + sequences will take several minutes to align. +

      +
      + {is_protein && ( + <> + {' '} + {' '} + Protein{' '} + + )} + {is_protein && ( + <> + {' '} + CDS + (spliced){' '} + + )} + {' '} + Genomic + + {' '} + nt upstream (max 2500) + {' '} + nt downstream (max 2500) + +

      + Output format:   +

      - -
      - - ); + +

      + +
      + + ); } } } const TranscriptionSummaryForm = connect( ({ record }) => ({ - expressionGraphsTableState: record.eupathdb.tables?.ExpressionGraphs + expressionGraphsTableState: record.eupathdb.tables?.ExpressionGraphs, }), { updateSectionVisibility: RecordActions.updateSectionVisibility, - updateTableState - } -)(class TranscriptionSummaryFormPres extends SortKeyTable { - constructor(props) { - super(props); - this.state = { - summaryIframeState: { - isLoading: true, - isError: false, - }, - }; - - this._makeIframeUrl = this._makeIframeUrl.bind(this); - this._handleIframeLoad = this._handleIframeLoad.bind(this); + updateTableState, } - - componentDidUpdate(prevProps) { - if ( - this._makeIframeUrl(projectId, prevProps.record.attributes.source_id) !== - this._makeIframeUrl(projectId, this.props.record.attributes.source_id) - ) { - this.setState({ +)( + class TranscriptionSummaryFormPres extends SortKeyTable { + constructor(props) { + super(props); + this.state = { summaryIframeState: { isLoading: true, isError: false, }, - }); - } - } - - _makeIframeUrl(projectId, sourceId) { - return ( - "/cgi-bin/dataPlotter.pl?project_id=" + - projectId + - "&id=" + - sourceId + - "&type=RNASeqTranscriptionSummary&template=1&datasetId=All&wl=0&facet=na&contXAxis=na&fmt=html" - ); - } - - _handleIframeLoad(loadEvent) { - loadEvent.target.contentDocument?.body.addEventListener('click', (e) => { - const { ExpressionGraphs } = this.props.record.tables; + }; - if (ExpressionGraphs == null) { - return; - } + this._makeIframeUrl = this._makeIframeUrl.bind(this); + this._handleIframeLoad = this._handleIframeLoad.bind(this); + } - // If a dataset entry was clicked... + componentDidUpdate(prevProps) { if ( - e.target.classList.contains('annotation-text') && - e.target.dataset.unformatted + this._makeIframeUrl( + projectId, + prevProps.record.attributes.source_id + ) !== + this._makeIframeUrl(projectId, this.props.record.attributes.source_id) ) { - // Find the associated expression graph row data - // FIXME: Look up the expression graph entry by dataset_id instead of display_name - // This will require adding the dataset_id as a data attribute - const expressionGraphIndex = ExpressionGraphs.findIndex( - ({ display_name }) => e.target.dataset.unformatted.startsWith(display_name) - ); + this.setState({ + summaryIframeState: { + isLoading: true, + isError: false, + }, + }); + } + } + + _makeIframeUrl(projectId, sourceId) { + return ( + '/cgi-bin/dataPlotter.pl?project_id=' + + projectId + + '&id=' + + sourceId + + '&type=RNASeqTranscriptionSummary&template=1&datasetId=All&wl=0&facet=na&contXAxis=na&fmt=html' + ); + } + + _handleIframeLoad(loadEvent) { + loadEvent.target.contentDocument?.body.addEventListener('click', (e) => { + const { ExpressionGraphs } = this.props.record.tables; - const expressionGraphTableElement = document.getElementById('ExpressionGraphs'); + if (ExpressionGraphs == null) { + return; + } - // If the expression graph table is available... + // If a dataset entry was clicked... if ( - expressionGraphIndex !== -1 && - expressionGraphTableElement != null + e.target.classList.contains('annotation-text') && + e.target.dataset.unformatted ) { - // Open the ExpressionGraphs record section - this.props.updateSectionVisibility('ExpressionGraphs', true); - - // Scroll to the ExpressionGraphs record section - const position = expressionGraphTableElement.getBoundingClientRect(); - scrollTo( - position.top + window.scrollY, - // When the scroll is complete, clear the table's search term - // and "select" the expresion graph row - () => { - this.props.updateTableState( - 'ExpressionGraphs', - { + // Find the associated expression graph row data + // FIXME: Look up the expression graph entry by dataset_id instead of display_name + // This will require adding the dataset_id as a data attribute + const expressionGraphIndex = ExpressionGraphs.findIndex( + ({ display_name }) => + e.target.dataset.unformatted.startsWith(display_name) + ); + + const expressionGraphTableElement = + document.getElementById('ExpressionGraphs'); + + // If the expression graph table is available... + if ( + expressionGraphIndex !== -1 && + expressionGraphTableElement != null + ) { + // Open the ExpressionGraphs record section + this.props.updateSectionVisibility('ExpressionGraphs', true); + + // Scroll to the ExpressionGraphs record section + const position = + expressionGraphTableElement.getBoundingClientRect(); + scrollTo( + position.top + window.scrollY, + // When the scroll is complete, clear the table's search term + // and "select" the expresion graph row + () => { + this.props.updateTableState('ExpressionGraphs', { ...this.props.expressionGraphsTableState, searchTerm: '', selectedRow: { index: expressionGraphIndex, - id: `ExpressionGraphs__${ExpressionGraphs[expressionGraphIndex].dataset_id}` - } - } - ); - } - ); + id: `ExpressionGraphs__${ExpressionGraphs[expressionGraphIndex].dataset_id}`, + }, + }); + } + ); + } } - } - }); + }); - this.setState({ - summaryIframeState: { - isLoading: false, - isError: false, - }, - }); - } + this.setState({ + summaryIframeState: { + isLoading: false, + isError: false, + }, + }); + } - render() { - let { source_id } = this.props.record.attributes; + render() { + let { source_id } = this.props.record.attributes; + + let height = 700; + if (this.props.value.length === 0) { + return ( +

      + No data available +

      + ); + } else { + if ((this.props.value.length + 1) * 40 > 700) { + height = (this.props.value.length + 1) * 40; + } + } - let height = 700; - if (this.props.value.length === 0) { return ( -

      No data available

      +
      + + + +
      ); - } else { - if (((this.props.value.length + 1) * 40) > 700) { - height = (this.props.value.length + 1) * 40; - } } - - return ( -
      - - - -
      - ); } -}); +); -const UserCommentsTable = addCommentLink(props => props.record.attributes.user_comment_link_url); +const UserCommentsTable = addCommentLink( + (props) => props.record.attributes.user_comment_link_url +); /** * Adaptation of https://stackoverflow.com/a/55686711 @@ -1228,16 +1533,14 @@ const UserCommentsTable = addCommentLink(props => props.record.attributes.user_c * @param yOffset - vertical offset to scroll to * @param callback - callback function */ - function scrollTo(yOffset, callback) { +function scrollTo(yOffset, callback) { const fixedYOffset = yOffset.toFixed(); - const onScroll = function() { - if ( - window.pageYOffset.toFixed() === fixedYOffset - ) { + const onScroll = function () { + if (window.pageYOffset.toFixed() === fixedYOffset) { window.removeEventListener('scroll', onScroll); callback(); } - } + }; window.addEventListener('scroll', onScroll); onScroll(); @@ -1246,7 +1549,6 @@ const UserCommentsTable = addCommentLink(props => props.record.attributes.user_c }); } - class CellxgeneIframe extends SortKeyTable { constructor(props) { super(props); @@ -1279,33 +1581,20 @@ class CellxgeneIframe extends SortKeyTable { _makeIframeUrl(dataset_name, sourceIds) { const appUrl = this._makeGeneAppUrl(dataset_name, sourceIds); - return ( - appUrl + - "&compact=1" - ); + return appUrl + '&compact=1'; } _makeAppUrl(dataset_name) { - return ( - "/cellxgene/view/" + - dataset_name + - ".h5ad/" - ); + return '/cellxgene/view/' + dataset_name + '.h5ad/'; } _makeGeneAppUrl(dataset_name, sourceIds) { - const sourceIdAr = sourceIds.split(";"); + const sourceIdAr = sourceIds.split(';'); const appUrl = this._makeAppUrl(dataset_name); - return ( - appUrl + - "?gene=" + - sourceIdAr[0] - ); + return appUrl + '?gene=' + sourceIdAr[0]; } - _handleIframeLoad(loadEvent) { - this.setState({ CellxgeneIframeState: { isLoading: false, @@ -1320,9 +1609,9 @@ class CellxgeneIframe extends SortKeyTable { let height = 350; let width = 800; console.log(this.props); - const sourceIdAr = source_ids.split(";"); + const sourceIdAr = source_ids.split(';'); - /* if (this.props.value.length === 0) { + /* if (this.props.value.length === 0) { return (

      No data available

      ); @@ -1350,16 +1639,50 @@ class CellxgeneIframe extends SortKeyTable { >
      - Left: A UMAP where each point is a cell colored by the normalized expression value for this gene. Right: A histogram showing the distribution of normalized expression values for this gene over all cells.

      Explore source identifiers mapped to {source_id} in cellxgene. -
        - {sourceIdAr.map((id, i) => { - return (
      • {id}
      • ) - })} -
      -

      - Explore the full dataset in cellxgene.

      - For help using cellxgene see this tutorial or this YouTube video.

      -
      + Left: A UMAP where each point is a cell colored by the + normalized expression value for this gene. Right: A histogram + showing the distribution of normalized expression values for this gene + over all cells. +
      +
      + Explore source identifiers mapped to {source_id} in cellxgene. + +
        + {sourceIdAr.map((id, i) => { + return ( +
      • + + {id} + +
      • + ); + })} +
      +
      +
      + + Explore the full dataset in cellxgene + + . +
      +
      + For help using cellxgene see{' '} + + this tutorial + {' '} + or{' '} + + this YouTube video + + .
      +
      +
      ); } diff --git a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/main.js b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/main.js index 27c3bf8602..d345da759b 100644 --- a/packages/sites/genomics-site/webapp/wdkCustomization/js/client/main.js +++ b/packages/sites/genomics-site/webapp/wdkCustomization/js/client/main.js @@ -1,7 +1,3 @@ import { initialize } from './bootstrap'; initialize(); - -// Using dynamic import to lazy load these scripts -import('../../../../vendored/pdbe-molstar-component-3.0.0'); -import('../../../../vendored/pdbe-molstar-light-3.0.0.css');