diff --git a/package.json b/package.json index efcdc66f..2a4ef6e9 100644 --- a/package.json +++ b/package.json @@ -8,22 +8,29 @@ "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", + "antd": "^4.21.7", + "antd-shopping-cart": "^1.0.7", "axios": "^0.27.2", - "antd": "^4.20.6", "classnames": "^2.3.1", "elasticlunr": "^0.9.5", "eslint": "^7.27.0", "helx-analytics": "^1.0.14", + "js-file-download": "^0.4.12", "lunr": "^2.3.9", + "rc-queue-anim": "^2.0.0", + "rc-texty": "^0.2.0", + "rc-tween-one": "^3.0.6", "react": "^17.0.2", "react-dom": "^17.0.2", "react-highlight-words": "^0.18.0", "react-infinite-scroll-component": "^6.1.0", - "react-scripts": "4.0.3", + "react-scripts": "^5.0.1", + "react-sizeme": "^3.0.2", "styled-components": "^5.3.0", "timeago-react": "^3.0.2", "use-debounce": "^7.0.0", - "web-vitals": "^1.0.1" + "web-vitals": "^1.0.1", + "yaml": "^2.1.1" }, "scripts": { "start": "react-scripts start", @@ -49,5 +56,6 @@ "last 1 firefox version", "last 1 safari version" ] - } + }, + "devDependencies": {} } diff --git a/src/app.js b/src/app.js index 0684d9c0..a6d9d78b 100644 --- a/src/app.js +++ b/src/app.js @@ -1,21 +1,28 @@ import { useEffect } from 'react' import { LocationProvider, Router as ReachRouter, globalHistory, useLocation } from '@reach/router' -import { EnvironmentProvider, ActivityProvider, AppProvider, InstanceProvider, AnalyticsProvider, useEnvironment, useAnalytics } from './contexts' +import { + EnvironmentProvider, ActivityProvider, AppProvider, + InstanceProvider, AnalyticsProvider, ShoppingCartProvider, + useEnvironment, useAnalytics +} from './contexts' import { Layout } from './components/layout' import { NotFoundView } from './views' +import 'antd-shopping-cart/dist/bundle.css' const ContextProviders = ({ children }) => { return ( - - - - {children} - - - + + + + + {children} + + + + diff --git a/src/components/layout/layout.css b/src/components/layout/layout.css index a6ca167b..4ccdb441 100644 --- a/src/components/layout/layout.css +++ b/src/components/layout/layout.css @@ -1,5 +1,13 @@ .helx-header { padding-right: 2px !important; + isolation: isolate; + position: sticky; + top: 0; + box-shadow: 0px 2px 6px 4px rgba(0,0,0,0.1); +} + +.layout { + isolation: isolate; } .brand_img { @@ -24,6 +32,23 @@ position: absolute; } -.logout-button { - margin: 0 1rem 0 0.5rem; +.shopping-cart-button { + margin-left: 8px !important; + /* The header has 2px of right padding, so this corresponds to 16px (aligned halfway between main content and page end) */ + margin-right: 14px !important; +} + +.shopping-cart-button, .logout-button { + /* margin-left: 16px; */ +} + +.icon-btn { + cursor: pointer; + transition: color 0.3s ease; +} +.icon-btn:not(.no-hover):hover { + color: #40a9ff !important; } +.icon-btn:active { + color: #096dd9 !important; +} \ No newline at end of file diff --git a/src/components/layout/layout.js b/src/components/layout/layout.js index 71da97c2..23e731ef 100644 --- a/src/components/layout/layout.js +++ b/src/components/layout/layout.js @@ -1,20 +1,40 @@ -import { Layout as AntLayout, Button, Menu, Grid } from 'antd' -import { useLocation, Link } from '@reach/router' +import { useEffect, useState } from 'react'; +import { Layout as AntLayout, Button, Menu, Grid, Divider, Badge, Popover, Typography, Tag, Space } from 'antd' +import { ShoppingCartOutlined as ShoppingCartIcon } from '@ant-design/icons' +import { useLocation, Link, redirectTo, navigate } from '@reach/router' import { useEnvironment, useAnalytics } from '../../contexts'; import { logoutHandler } from '../../api/'; import { MobileMenu } from './menu'; import { SidePanel } from '../side-panel/side-panel'; +import { CartPopoverButton, useShoppingCart } from 'antd-shopping-cart'; import './layout.css'; +const { Text, Title } = Typography const { Header, Content, Footer } = AntLayout const { useBreakpoint } = Grid +const ACTIVE_CART_LS_KEY = 'active_cart'; + export const Layout = ({ children }) => { const { helxAppstoreUrl, routes, context, basePath } = useEnvironment() const { analyticsEvents } = useAnalytics() const { md } = useBreakpoint() const baseLinkPath = context.workspaces_enabled === 'true' ? '/helx' : '' const location = useLocation(); + const { activeCart, setActiveCart } = useShoppingCart(); + + // on mount, check if there is a active cart in local storage + useEffect(() => { + const stored = window.localStorage.getItem(ACTIVE_CART_LS_KEY); + if (stored) setActiveCart(stored); + }, []); + + // anytime the activeCart is changed, update localStorage + useEffect(() => { + window.localStorage.setItem(ACTIVE_CART_LS_KEY, activeCart.name); + }, [activeCart]); + + const logoutButton = context.workspaces_enabled === 'true' const logout = () => { analyticsEvents.logout() @@ -23,7 +43,7 @@ export const Layout = ({ children }) => { return ( -
+
{context !== undefined ? {context.brand} : } {md ? (
@@ -41,9 +61,26 @@ export const Layout = ({ children }) => { {m.text} ))} - {context.workspaces_enabled === 'true' && ( + {/* */} +
+ navigate(`${baseLinkPath}/cart`) } + /> +
+ {logoutButton && (
- +
)}
@@ -51,7 +88,7 @@ export const Layout = ({ children }) => { )}
- + location.pathname === `${ baseLinkPath }${ route.path }`)?.path }> {children} {context.workspaces_enabled === 'true' && } diff --git a/src/components/search/concept-card/concept-card.js b/src/components/search/concept-card/concept-card.js index 383caa0b..5317424a 100644 --- a/src/components/search/concept-card/concept-card.js +++ b/src/components/search/concept-card/concept-card.js @@ -1,13 +1,14 @@ -import { Fragment, useState, useEffect, forwardRef } from 'react' -import PropTypes from 'prop-types' +import { useState, forwardRef } from 'react' import { Badge, Card, Space, Typography } from 'antd' import { ExpandOutlined as ViewIcon } from '@ant-design/icons' -import { useHelxSearch } from '../' +import { AddToCartIconButton, useShoppingCart } from 'antd-shopping-cart' +import PropTypes from 'prop-types' +import classNames from 'classnames' import { OverviewTab } from './overview-tab' import { StudiesTab } from './studies-tab' -// import { CdesTab } from './cdes-tab' +import { useHelxSearch } from '../' import { useAnalytics } from '../../../contexts' -import classNames from 'classnames' +import { useShoppingCartUtilities } from '../../../hooks' import './concept-card.css' const { Text } = Typography @@ -19,6 +20,8 @@ export const ConceptCard = forwardRef(({ index, result, openModalHandler, icon=V const { query } = useHelxSearch() const [currentTab, setCurrentTab] = useState('overview') + const { createConceptCartItem } = useShoppingCartUtilities() + const tabs = { 'overview': { title: 'Overview', content: }, 'studies': { title: `Studies`, content: }, @@ -51,8 +54,14 @@ export const ConceptCard = forwardRef(({ index, result, openModalHandler, icon=V tabProps={{size: 'small'}} activeTabKey={currentTab} onTabChange={key => setCurrentTab(key)} - extra={ icon && } + extra={ +
+ + { icon && } +
+ } actions={ [
] } + // style={{ border: isConceptInCart(activeCart, result) ? "1px solid #91d5ff" : undefined }} > { tabContents[currentTab] } diff --git a/src/components/search/concept-card/studies-tab.js b/src/components/search/concept-card/studies-tab.js index e7d08ebd..9e3f078a 100644 --- a/src/components/search/concept-card/studies-tab.js +++ b/src/components/search/concept-card/studies-tab.js @@ -1,13 +1,16 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import { List, Spin, Space, Tag, Typography, Divider } from 'antd' +import { AddToCartIconButton } from 'antd-shopping-cart' import { useHelxSearch } from '../' import { Link } from '../../link' +import { useShoppingCartUtilities } from '../../../hooks' const { Text } = Typography const { CheckableTag: CheckableFacet } = Tag export const StudiesTab = ({ result }) => { const { query, fetchStudyVariables, fetchCDEs } = useHelxSearch() + const { createStudyCartItem } = useShoppingCartUtilities() const [studies, setStudies] = useState([]) const [loading, setLoading] = useState(true) const [facets, setFacets] = useState([]) @@ -94,6 +97,7 @@ export const StudiesTab = ({ result }) => { { study.elements && `${ study.elements.length } variable${ study.elements.length === 1 ? '' : 's'}` } + { study.elements && } ) } /> diff --git a/src/components/search/concept-modal/concept-modal.css b/src/components/search/concept-modal/concept-modal.css index 62f82025..7afaa15b 100644 --- a/src/components/search/concept-modal/concept-modal.css +++ b/src/components/search/concept-modal/concept-modal.css @@ -32,10 +32,13 @@ } .study-variables-list-item { + display: flex; + flex-direction: column; margin-left: 4px; padding-left: 1rem !important; border-left: 2px solid #eee; margin-bottom: 0.5rem; + gap: 2px !important; } .variable-name { diff --git a/src/components/search/concept-modal/concept-modal.js b/src/components/search/concept-modal/concept-modal.js index c78e9941..2fc78088 100644 --- a/src/components/search/concept-modal/concept-modal.js +++ b/src/components/search/concept-modal/concept-modal.js @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react' -import { Button, Menu, Modal, Result, Space, Spin, Typography } from 'antd' -import CustomIcon, { +import { Menu, Modal, Space, Button, Result, Spin, Typography } from 'antd' +import { InfoCircleOutlined as OverviewIcon, BookOutlined as StudiesIcon, ShareAltOutlined as KnowledgeGraphsIcon, @@ -14,7 +14,9 @@ import CustomIcon, { import { CdesTab, OverviewTab, StudiesTab, KnowledgeGraphsTab, TranQLTab } from './tabs' import { useHelxSearch } from '../' import { useAnalytics, useEnvironment } from '../../../contexts' +import { useShoppingCartUtilities } from '../../../hooks' import './concept-modal.css' +import { AddToCartDropdownButton } from 'antd-shopping-cart' const { Text, Paragraph } = Typography @@ -56,7 +58,7 @@ export const ConceptModalBody = ({ result }) => { const tabs = { 'overview': { title: 'Overview', icon: , content: , }, 'studies': { title: 'Studies', icon: , content: , }, - 'cdes': { title: `CDEs`, icon: , content: }, + 'cdes': { title: `CDEs`, icon: , content: }, 'kgs': { title: 'Knowledge Graphs', icon: , content: , }, 'tranql': { title: 'TranQL', icon: , content: }, // 'robokop': { title: 'Robokop', icon: , content: } @@ -274,9 +276,11 @@ export const ConceptModalBody = ({ result }) => { } export const ConceptModal = ({ result, visible, closeHandler }) => { - const { setFullscreenResult, setSelectedResult } = useHelxSearch() + const { setFullscreenResult, query, setSelectedResult } = useHelxSearch() const [doFullscreen, setDoFullscreen] = useState(null) + const { createConceptCartItem } = useShoppingCartUtilities() + /** * Essentially peforming the same thing as what a this.setState call using a callback would do. * Need to render the modal as closed for one render in order for the modal to actually close @@ -310,10 +314,15 @@ export const ConceptModal = ({ result, visible, closeHandler }) => { bodyStyle={{ padding: `0`, minHeight: `50vh`, position: `relative` }} cancelButtonProps={{ hidden: true }} footer={( - - - - +
+ + + + + +
)} > diff --git a/src/components/search/concept-modal/tabs/cdes/cde-item.js b/src/components/search/concept-modal/tabs/cdes/cde-item.js index 2f7928fa..af76d781 100644 --- a/src/components/search/concept-modal/tabs/cdes/cde-item.js +++ b/src/components/search/concept-modal/tabs/cdes/cde-item.js @@ -3,6 +3,8 @@ import { List, Collapse, Typography, Space, Button } from 'antd' import { ExportOutlined } from '@ant-design/icons' import _Highlighter from 'react-highlight-words' import { RelatedConceptsList } from './related-concepts' +import { AddToCartIconButton } from 'antd-shopping-cart'; +import { useShoppingCartUtilities } from '../../../../../hooks'; const { Text, Link } = Typography const { Panel } = Collapse @@ -17,8 +19,9 @@ const Section = ({ title, children }) => ( ) -export const CdeItem = ({ cde, cdeRelatedConcepts, highlight }) => { +export const CdeItem = ({ cde, cdeRelatedConcepts, highlight, result }) => { const [collapsed, setCollapsed] = useState(false) + const { createCdeCartItem } = useShoppingCartUtilities() const relatedConceptsSource = useMemo(() => ( cdeRelatedConcepts[cde.id] @@ -42,6 +45,10 @@ export const CdeItem = ({ cde, cdeRelatedConcepts, highlight }) => { {/* Only show the "Open study" button if the CDE has a link attached to it. */} + {cde.e_link && (