@@ -38,6 +54,7 @@ export const Study = ({ study, highlight, collapsed, ...panelProps }) => {
highlightedVariables.map((variable) => (
diff --git a/src/components/shopping-cart/index.js b/src/components/shopping-cart/index.js
new file mode 100644
index 00000000..e8887b4b
--- /dev/null
+++ b/src/components/shopping-cart/index.js
@@ -0,0 +1 @@
+export * from './shopping-cart'
\ No newline at end of file
diff --git a/src/components/shopping-cart/shopping-cart.css b/src/components/shopping-cart/shopping-cart.css
new file mode 100644
index 00000000..89d1f2c9
--- /dev/null
+++ b/src/components/shopping-cart/shopping-cart.css
@@ -0,0 +1,21 @@
+/** Rules for getting overflow into the flex display rather than the page itself. */
+.layout .ant-layout-content[data-active-route="/cart"] {
+ flex-grow: 1;
+}
+.cart-list-layout {
+ height: 0;
+}
+.cart-list-layout > * {
+ background: #fff !important;
+}
+.cart-list-layout .cart-layout-content > *, .cart-list-layout .cart-layout-content > * > * {
+ height: 100%;
+}
+.cart-list-layout .cart-layout-content {
+ flex-grow: 1;
+ height: 0;
+ /* margin-bottom: -16px; */
+}
+.cart-list-layout .cart-list-extra {
+ margin-bottom: -16px;
+}
\ No newline at end of file
diff --git a/src/components/shopping-cart/shopping-cart.js b/src/components/shopping-cart/shopping-cart.js
new file mode 100644
index 00000000..ea94912b
--- /dev/null
+++ b/src/components/shopping-cart/shopping-cart.js
@@ -0,0 +1,170 @@
+import { Fragment, useEffect, useMemo, useState } from 'react'
+import { Space, Layout, Typography, Menu, Modal, Checkbox, Select, notification } from 'antd'
+import { ShoppingCartOutlined, LoadingOutlined } from '@ant-design/icons'
+import { CartListLayout, useShoppingCart } from 'antd-shopping-cart'
+import YAML from 'yaml'
+import download from 'js-file-download'
+import './shopping-cart.css'
+
+const { Title, Text } = Typography
+const { Sider, Content } = Layout
+const { Option } = Select
+
+const ExportFormats = {
+ JSON: {
+ name: "JSON"
+ },
+ YAML: {
+ name: "YAML"
+ },
+ CSV: {
+ name: "CSV"
+ }
+}
+
+
+export const ShoppingCart = () => {
+ const { buckets, carts, activeCart, setActiveCart, updateCart } = useShoppingCart()
+ const [exportItems, setExportItems] = useState([])
+ const [exportFormat, setExportFormat] = useState("JSON")
+ const [exportReadable, setExportReadable] = useState(true)
+ const [deleteItemsAfterExport, setDeleteItemsAfterExport] = useState(true)
+ const [showExportModal, setShowExportModal] = useState(false)
+
+ const exportingFullCart = useMemo(() => exportItems.length === activeCart.items.length, [activeCart, exportItems])
+
+ useEffect(() => {
+ if (!showExportModal) {
+ setExportItems([])
+ setExportFormat("JSON")
+ setExportReadable(true)
+ setDeleteItemsAfterExport(true)
+ }
+ }, [showExportModal])
+
+ return (
+
+ {
+ setExportItems(selectedItems.length === 0 ? activeCart.items : selectedItems )
+ setShowExportModal(true)
+ } }
+ cartListProps={{
+ extraProps: {
+ renderCheckoutText: (selectedCount) => selectedCount > 0 ? `Export ${ selectedCount } selected item${ selectedCount !== 1 ? "s" : "" }` : "Export"
+ }
+ }}
+ />
+ {
+ setShowExportModal(false)
+ notification.open({
+ message: "Download will begin shortly...",
+ description: "",
+ duration: 1.5,
+ icon: ,
+ placement: "bottomLeft"
+ })
+ const date = new Date()
+ const name = `${ activeCart.name }_${ (date.getMonth() + 1) + "-" + date.getDate() + "-" + date.getFullYear() }`
+ let fileName
+
+ const items = exportItems.length > 0 ? exportItems : activeCart.items;
+
+ const cart = {
+ concept_id: [],
+ study_id: [],
+ variable_id: [],
+ cde_id: [],
+ }
+
+ items.forEach(({bucketId, id}) => {
+ switch (bucketId) {
+ case 'concepts': cart.concept_id.push(id); break;
+ case 'studies': cart.study_id.push(id); break;
+ case 'variables': cart.variable_id.push(id); break;
+ case 'cdes': cart.cde_id.push(id); break;
+ default: break;
+ }
+ })
+
+ let data
+ if (exportFormat === "JSON") {
+ fileName = `${ name }.json`
+ data = exportReadable ? JSON.stringify(
+ cart,
+ undefined,
+ 4
+ ) : JSON.stringify(cart)
+ } else if (exportFormat === "YAML") {
+ fileName = `${ name }.yaml`
+ data = YAML.stringify(cart)
+ } else if (exportFormat === "CSV") {
+ fileName = `${ name }.csv`
+
+ data = Object.keys(cart).join(',') + '\n';
+
+ const getRow = (i) => Object.keys(cart).map((col) => cart[col][i]);
+
+ for (let i = 0; getRow(i).some((col) => col !== undefined); ++i) {
+ data += getRow(i).join(',') + '\n';
+ }
+ }
+
+ if (deleteItemsAfterExport) {
+ updateCart(activeCart, {
+ items: activeCart.items.filter((item) => !exportItems.find((_item) => _item.id === item.id))
+ })
+ }
+
+ setTimeout(() => download(
+ data,
+ fileName
+ ), 2000)
+ } }
+ onCancel={ () => setShowExportModal(false) }
+ zIndex={1032}
+ maskStyle={{ zIndex: 1031 }}
+ >
+
+ Exporting from { activeCart.name } - { exportItems.length } items
+
+
+
+
+ Export format:
+
+
+
+
+ setExportReadable(!exportReadable) }>
+ Export in human-readable format
+
+
+
+ setDeleteItemsAfterExport(!deleteItemsAfterExport) }>
+ { exportingFullCart ? "Empty cart after export" : "Remove items from cart after export" }
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/contexts/analytics-context/analytics-context.js b/src/contexts/analytics-context/analytics-context.js
index ffde2b74..0f1ba6b3 100644
--- a/src/contexts/analytics-context/analytics-context.js
+++ b/src/contexts/analytics-context/analytics-context.js
@@ -2,9 +2,10 @@ import React, { createContext, useContext } from 'react';
import { useEnvironment } from '../environment-context';
import { AnalyticsEvents } from './events';
import { MixPanelAnalytics, GAAnalytics, NoAnalytics } from 'helx-analytics';
-import { version } from 'helx-analytics/package.json';
import { getUser } from '../../api';
+const version = require('helx-analytics/package.json').version
+
export const AnalyticsContext = createContext();
export const AnalyticsProvider = ({ children }) => {
diff --git a/src/contexts/environment-context.js b/src/contexts/environment-context.js
index b4a0befe..4589d515 100644
--- a/src/contexts/environment-context.js
+++ b/src/contexts/environment-context.js
@@ -6,7 +6,8 @@ import {
SupportView,
LoadingView,
SearchView,
- SplashScreenView
+ SplashScreenView,
+ ShoppingCartView,
} from '../views'
// Setup global csrf token
@@ -60,6 +61,7 @@ export const EnvironmentProvider = ({ children }) => {
baseRoutes.push({ path: '/', text: '', Component: SupportView })
}
baseRoutes.push({ path: '/support', text: 'Support', Component: SupportView })
+ baseRoutes.push({ path: '/cart', text: '', Component: ShoppingCartView })
return baseRoutes;
}
diff --git a/src/contexts/index.js b/src/contexts/index.js
index 4e8d9172..9b2b753a 100644
--- a/src/contexts/index.js
+++ b/src/contexts/index.js
@@ -3,4 +3,5 @@ export * from './auth-context'
export * from './environment-context'
export * from './instance-context'
export * from './activity-context'
-export * from './analytics-context'
\ No newline at end of file
+export * from './analytics-context'
+export * from './shopping-cart-context'
\ No newline at end of file
diff --git a/src/contexts/shopping-cart-context.js b/src/contexts/shopping-cart-context.js
new file mode 100644
index 00000000..3c22cca8
--- /dev/null
+++ b/src/contexts/shopping-cart-context.js
@@ -0,0 +1,38 @@
+import { ShoppingCartProvider as _ShoppingCartProvider } from 'antd-shopping-cart'
+import { useEnvironment } from './environment-context'
+
+export const ShoppingCartProvider = ({ children }) => {
+ const { helxSearchUrl } = useEnvironment()
+
+ return (
+ <_ShoppingCartProvider
+ defaultCartName="My cart"
+ localStorageKey="shopping_carts"
+ helxSearchUrl={helxSearchUrl}
+ buckets={[
+ {
+ id: "concepts",
+ name: "Concepts",
+ itemName: "concept"
+ },
+ {
+ id: "studies",
+ name: "Studies",
+ itemName: "study"
+ },
+ {
+ id: "variables",
+ name: "Variables",
+ itemName: "variable"
+ },
+ {
+ id: "cdes",
+ name: "CDEs",
+ itemName: "cde"
+ },
+ ]}
+ >
+ { children }
+
+ )
+}
\ No newline at end of file
diff --git a/src/hooks/index.js b/src/hooks/index.js
index 5b7d9726..cc7ea2d4 100644
--- a/src/hooks/index.js
+++ b/src/hooks/index.js
@@ -3,3 +3,5 @@ export * from './use-local-storage'
export * from './use-search-index'
export * from './use-lunr-search'
export * from './use-is-scrollable'
+export * from './use-local-storage'
+export * from './use-shopping-cart-utils'
\ No newline at end of file
diff --git a/src/hooks/use-shopping-cart-utils.js b/src/hooks/use-shopping-cart-utils.js
new file mode 100644
index 00000000..64b3d84a
--- /dev/null
+++ b/src/hooks/use-shopping-cart-utils.js
@@ -0,0 +1,53 @@
+const createConceptCartItem = (concept, fromSearch) => ({
+ id: concept.id,
+ name: `${concept.name} (${concept.type})`,
+ description: concept.description,
+ price: null,
+ tax: null,
+ quantity: 1,
+ from: { type: "search", value: fromSearch },
+ bucketId: "concepts",
+ item: concept,
+})
+const createStudyCartItem = (study, fromConcept) => ({
+ id: study.c_id,
+ name: study.c_name,
+ nameSecondary: `(${ study.elements.length } variable${study.elements.length !== 1 ? "s" : ""})`,
+ price: null,
+ tax: null,
+ quantity: 1,
+ from: { type: "concept", value: fromConcept },
+ bucketId: "studies",
+ item: study,
+})
+const createVariableCartItem = (variable, fromStudy) => ({
+ id: variable.id,
+ name: variable.name,
+ description: variable.description,
+ price: null,
+ tax: null,
+ quantity: 1,
+ from: { type: "study", value: fromStudy },
+ bucketId: "variables",
+ item: variable,
+})
+const createCdeCartItem = (cde, fromConcept) => ({
+ id: cde.id,
+ name: cde.name,
+ description: cde.description,
+ price: null,
+ tax: null,
+ quantity: 1,
+ from: { type: "concept", value: fromConcept },
+ bucketId: "cdes",
+ item: cde,
+});
+
+export const useShoppingCartUtilities = () => {
+ return {
+ createConceptCartItem,
+ createStudyCartItem,
+ createVariableCartItem,
+ createCdeCartItem,
+ }
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 5d845d72..a39611e8 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,4 +1,5 @@
-@import url('antd/dist/antd.css');
+@import 'antd/dist/antd.css';
+@import 'rc-texty/assets/index.css';
*, *::before, *::after {
box-sizing: border-box;
diff --git a/src/views/cart.js b/src/views/cart.js
new file mode 100644
index 00000000..1cde0aab
--- /dev/null
+++ b/src/views/cart.js
@@ -0,0 +1,25 @@
+import { Fragment, useEffect } from 'react'
+import { useEnvironment } from '../contexts'
+import { Breadcrumbs } from '../components/layout'
+import { ShoppingCart } from '../components/shopping-cart'
+
+export const ShoppingCartView = () => {
+ const { context } = useEnvironment()
+
+ const breadcrumbs = [
+ { text: 'Home', path: '/helx' },
+ // { text: 'Search', path: '/helx/search' },
+ { text: 'Cart', path: '/cart' },
+ ]
+
+ useEffect(() => {
+ document.title = `Shopping Cart ยท HeLx UI`
+ }, [])
+
+ return (
+
+ { context.workspaces_enabled === 'true' && }
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/views/index.js b/src/views/index.js
index 46f93ad1..f7eeca23 100644
--- a/src/views/index.js
+++ b/src/views/index.js
@@ -1,6 +1,7 @@
-export * from './support'
+export * from './cart'
+export * from './loading'
export * from './not-found'
export * from './search'
+export * from './splash-screen'
+export * from './support'
export * from './workspaces'
-export * from './loading'
-export * from './splash-screen'
\ No newline at end of file