diff --git a/CHANGELOG-panel-list-landing-pages.md b/CHANGELOG-panel-list-landing-pages.md
new file mode 100644
index 0000000000..846b4626f7
--- /dev/null
+++ b/CHANGELOG-panel-list-landing-pages.md
@@ -0,0 +1 @@
+- Refactor panel components for reuse in upcoming publication pages.
\ No newline at end of file
diff --git a/context/app/static/js/components/CollectionsPanelList/CollectionsPanelList.jsx b/context/app/static/js/components/CollectionsPanelList/CollectionsPanelList.jsx
deleted file mode 100644
index b96c116961..0000000000
--- a/context/app/static/js/components/CollectionsPanelList/CollectionsPanelList.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-import { Panel, PanelScrollBox } from 'js/shared-styles/panels';
-
-function CollectionsPanelList({ collectionsData }) {
- return (
-
- {collectionsData.map(({ _source }) => (
-
- ))}
-
- );
-}
-
-export default CollectionsPanelList;
diff --git a/context/app/static/js/components/CollectionsPanelList/index.js b/context/app/static/js/components/CollectionsPanelList/index.js
deleted file mode 100644
index 899ad0a68f..0000000000
--- a/context/app/static/js/components/CollectionsPanelList/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import CollectionsPanelList from './CollectionsPanelList';
-
-export default CollectionsPanelList;
diff --git a/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.jsx b/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.jsx
index 9879c150ec..e128587e67 100644
--- a/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.jsx
+++ b/context/app/static/js/components/detailPage/CollectionsSection/CollectionsSection.jsx
@@ -2,13 +2,16 @@ import React from 'react';
import SectionHeader from 'js/shared-styles/sections/SectionHeader';
import { DetailPageSection } from 'js/components/detailPage/style';
-import CollectionsPanelList from 'js/components/CollectionsPanelList';
+import { buildCollectionsPanelsProps } from 'js/pages/Collections/utils';
+
+import PanelList from 'js/shared-styles/panels/PanelList';
function CollectionsSection({ collectionsData }) {
+ const panelsProps = buildCollectionsPanelsProps(collectionsData);
return (
Collections
-
+ ;
);
}
diff --git a/context/app/static/js/pages/Collections/Collections.jsx b/context/app/static/js/pages/Collections/Collections.jsx
index 1e933ba177..28a5aceb5b 100644
--- a/context/app/static/js/pages/Collections/Collections.jsx
+++ b/context/app/static/js/pages/Collections/Collections.jsx
@@ -1,29 +1,20 @@
import React from 'react';
-import Typography from '@material-ui/core/Typography';
-import CollectionsPanelList from 'js/components/CollectionsPanelList';
-import { useSearchHits } from 'js/hooks/useSearchData';
-import { getAllCollectionsQuery } from 'js/helpers/queries';
-import { PageWrapper, StyledDescription } from './style';
+import PanelListLandingPage from 'js/shared-styles/panels/PanelListLandingPage';
+import { useCollections } from './hooks';
-function Collections() {
- const { searchHits: collectionsData } = useSearchHits(getAllCollectionsQuery);
+const description =
+ 'Collections of HuBMAP datasets represent data from related experiments—such as assays performed on the same organ—or data that has been grouped for other reasons. In the future, it will be possible to reference collections through Document Object Identifiers (DOIs).';
+function Collections() {
+ const panelsProps = useCollections();
return (
-
-
- Collections
-
-
- {collectionsData.length > 0 && `${collectionsData.length} Collections`}
-
-
- Collections of HuBMAP datasets represent data from related experiments—such as assays performed on the same
- organ—or data that has been grouped for other reasons. In the future, it will be possible to reference
- collections through Document Object Identifiers (DOIs).
-
- {collectionsData.length > 0 && }
-
+ 0 && `${panelsProps.length} Collections`}
+ description={description}
+ panelsProps={panelsProps}
+ />
);
}
diff --git a/context/app/static/js/pages/Collections/hooks.js b/context/app/static/js/pages/Collections/hooks.js
new file mode 100644
index 0000000000..5c0b9515ea
--- /dev/null
+++ b/context/app/static/js/pages/Collections/hooks.js
@@ -0,0 +1,10 @@
+import { useSearchHits } from 'js/hooks/useSearchData';
+import { getAllCollectionsQuery } from 'js/helpers/queries';
+import { buildCollectionsPanelsProps } from './utils';
+
+function useCollections() {
+ const { searchHits: collectionsData } = useSearchHits(getAllCollectionsQuery);
+ return buildCollectionsPanelsProps(collectionsData);
+}
+
+export { useCollections };
diff --git a/context/app/static/js/pages/Collections/utils.js b/context/app/static/js/pages/Collections/utils.js
new file mode 100644
index 0000000000..39b195cfd2
--- /dev/null
+++ b/context/app/static/js/pages/Collections/utils.js
@@ -0,0 +1,14 @@
+import React from 'react';
+import Typography from '@material-ui/core/Typography';
+
+function buildCollectionsPanelsProps(collections) {
+ return collections.map(({ _source }) => ({
+ key: _source.uuid,
+ href: `/browse/collection/${_source.uuid}`,
+ title: _source.title,
+ secondaryText: _source.hubmap_id,
+ rightText: {`${_source.datasets.length} Datasets`},
+ }));
+}
+
+export { buildCollectionsPanelsProps };
diff --git a/context/app/static/js/pages/Dataset/Dataset.jsx b/context/app/static/js/pages/Dataset/Dataset.jsx
index ae1f0cb4e3..f7ca6a6992 100644
--- a/context/app/static/js/pages/Dataset/Dataset.jsx
+++ b/context/app/static/js/pages/Dataset/Dataset.jsx
@@ -18,8 +18,7 @@ import useEntityStore from 'js/stores/useEntityStore';
import CollectionsSection from 'js/components/detailPage/CollectionsSection';
import SupportAlert from 'js/components/detailPage/SupportAlert';
import { DetailPageAlert } from 'js/components/detailPage/style';
-import { useSearchHits } from 'js/hooks/useSearchData';
-import { getAllCollectionsQuery } from 'js/helpers/queries';
+
import { ReactComponent as WorkspacesIcon } from 'assets/svg/workspaces.svg';
import { WhiteBackgroundIconButton } from 'js/shared-styles/buttons';
import { SecondaryBackgroundTooltip } from 'js/shared-styles/tooltips';
@@ -28,8 +27,9 @@ import DetailContext from 'js/components/detailPage/context';
import { getSectionOrder } from 'js/components/detailPage/utils';
import CreateWorkspaceDialog from 'js/components/workspaces/CreateWorkspaceDialog';
-import { combineMetadata, getCollectionsWhichContainDataset } from 'js/pages/utils/entity-utils';
+import { combineMetadata } from 'js/pages/utils/entity-utils';
import OutboundIconLink from 'js/shared-styles/Links/iconLinks/OutboundIconLink';
+import { useDatasetCollections } from './hooks';
function NotebookButton(props) {
return (
@@ -142,8 +142,7 @@ function DatasetDetail({ assayMetadata, vitData, hasNotebook, visLiftedUUID }) {
const combinedMetadata = combineMetadata(donor, origin_sample, source_sample, metadata);
- const { searchHits: allCollections } = useSearchHits(getAllCollectionsQuery);
- const collectionsData = getCollectionsWhichContainDataset(uuid, allCollections);
+ const collectionsData = useDatasetCollections(uuid);
const shouldDisplaySection = {
provenance: entity_type !== 'Support',
diff --git a/context/app/static/js/pages/Dataset/hooks.js b/context/app/static/js/pages/Dataset/hooks.js
new file mode 100644
index 0000000000..9e8fe3ad9e
--- /dev/null
+++ b/context/app/static/js/pages/Dataset/hooks.js
@@ -0,0 +1,11 @@
+import { useSearchHits } from 'js/hooks/useSearchData';
+import { getAllCollectionsQuery } from 'js/helpers/queries';
+
+import { getCollectionsWhichContainDataset } from './utils';
+
+function useDatasetCollections(uuid) {
+ const { searchHits: collections } = useSearchHits(getAllCollectionsQuery);
+ return getCollectionsWhichContainDataset(uuid, collections);
+}
+
+export { useDatasetCollections };
diff --git a/context/app/static/js/pages/Dataset/utils.js b/context/app/static/js/pages/Dataset/utils.js
new file mode 100644
index 0000000000..11a1a8b450
--- /dev/null
+++ b/context/app/static/js/pages/Dataset/utils.js
@@ -0,0 +1,8 @@
+function getCollectionsWhichContainDataset(uuid, collections) {
+ return collections.filter((collection) => {
+ // eslint-disable-next-line no-underscore-dangle
+ return collection._source.datasets.some((dataset) => dataset.uuid === uuid);
+ });
+}
+
+export { getCollectionsWhichContainDataset };
diff --git a/context/app/static/js/pages/utils/entity-utils.js b/context/app/static/js/pages/utils/entity-utils.js
index 1cff7c0dae..7ee0db7cda 100644
--- a/context/app/static/js/pages/utils/entity-utils.js
+++ b/context/app/static/js/pages/utils/entity-utils.js
@@ -23,12 +23,4 @@ function combineMetadata(donor, origin_sample, source_sample, metadata) {
return combinedMetadata;
}
-
-function getCollectionsWhichContainDataset(uuid, collections) {
- return collections.filter((collection) => {
- // eslint-disable-next-line no-underscore-dangle
- return collection._source.datasets.some((dataset) => dataset.uuid === uuid);
- });
-}
-
-export { combineMetadata, getCollectionsWhichContainDataset };
+export { combineMetadata };
diff --git a/context/app/static/js/shared-styles/panels/Panel/Panel.jsx b/context/app/static/js/shared-styles/panels/Panel/Panel.jsx
new file mode 100644
index 0000000000..a15e9bc306
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/Panel/Panel.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { PanelBox, LeftTextWrapper, TruncatedLink, TruncatedText, RightTextWrapper } from './style';
+
+function Panel({ title, href, secondaryText, rightText }) {
+ return (
+
+
+
+ {title}
+
+
+ {secondaryText}
+
+
+ {rightText}
+
+ );
+}
+
+export default Panel;
diff --git a/context/app/static/js/shared-styles/panels/Panel/Panel.stories.js b/context/app/static/js/shared-styles/panels/Panel/Panel.stories.js
new file mode 100644
index 0000000000..ada0f38a4c
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/Panel/Panel.stories.js
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import PanelComponent from './Panel';
+
+export default {
+ title: 'Panels/Panel',
+ component: PanelComponent,
+};
+
+export const Panel = (args) => ;
+Panel.args = {
+ title: 'Title',
+ secondaryText: 'Secondary Text',
+ rightText: 'Right Text',
+};
+Panel.storyName = 'Panel'; // needed for single story hoisting for multi word component names
diff --git a/context/app/static/js/shared-styles/panels/Panel/index.js b/context/app/static/js/shared-styles/panels/Panel/index.js
new file mode 100644
index 0000000000..8858185a4d
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/Panel/index.js
@@ -0,0 +1,3 @@
+import Panel from './Panel';
+
+export default Panel;
diff --git a/context/app/static/js/shared-styles/panels/Panel/style.js b/context/app/static/js/shared-styles/panels/Panel/style.js
new file mode 100644
index 0000000000..0b89c5e0d9
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/Panel/style.js
@@ -0,0 +1,47 @@
+import styled, { css } from 'styled-components';
+import Typography from '@material-ui/core/Typography';
+
+import { LightBlueLink } from 'js/shared-styles/Links';
+
+const overflowCss = css`
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+`;
+
+const PanelBox = styled.div`
+ padding: 15px 20px;
+ border-bottom: 1px solid ${(props) => props.theme.palette.divider};
+ display: flex;
+ flex-direction: column;
+
+ &:hover {
+ background-color: ${(props) => props.theme.palette.hoverShadow.main};
+ }
+ @media (min-width: ${(props) => props.theme.breakpoints.values.md}px) {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+`;
+
+const LeftTextWrapper = styled.div`
+ white-space: nowrap;
+ min-width: 0px; // needed to handle overflow
+ margin-right: ${(props) => props.theme.spacing(1)}px;
+`;
+
+const TruncatedText = styled(Typography)`
+ ${overflowCss};
+`;
+
+const TruncatedLink = styled(LightBlueLink)`
+ ${overflowCss};
+ display: block; //text-overflow only applies to block elements
+`;
+
+const RightTextWrapper = styled.div`
+ flex-shrink: 0;
+ padding-left: ${(props) => props.theme.spacing(0.5)}px;
+`;
+
+export { PanelBox, LeftTextWrapper, TruncatedText, TruncatedLink, RightTextWrapper };
diff --git a/context/app/static/js/shared-styles/panels/PanelList/PanelList.jsx b/context/app/static/js/shared-styles/panels/PanelList/PanelList.jsx
new file mode 100644
index 0000000000..946580a093
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelList/PanelList.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+import Panel from 'js/shared-styles/panels/Panel';
+import { PanelScrollBox } from './style';
+
+function PanelList({ panelsProps }) {
+ return (
+
+ {panelsProps.map((props) => (
+
+ ))}
+
+ );
+}
+
+export default PanelList;
diff --git a/context/app/static/js/shared-styles/panels/PanelList/PanelList.stories.js b/context/app/static/js/shared-styles/panels/PanelList/PanelList.stories.js
new file mode 100644
index 0000000000..bf012026db
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelList/PanelList.stories.js
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import Panel from 'js/shared-styles/panels/Panel';
+import PanelListComponent from './PanelList';
+
+export default {
+ title: 'Panels/PanelList',
+ component: PanelListComponent,
+ subcomponents: { Panel },
+};
+
+export const PanelList = (args) => ;
+PanelList.args = {
+ panelsProps: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item, i) => ({
+ title: `Title ${i}`,
+ secondaryText: `Secondary Text ${i}`,
+ rightText: `Right Text ${i}`,
+ })),
+};
+PanelList.storyName = 'PanelList'; // needed for single story hoisting for multi word component names
diff --git a/context/app/static/js/shared-styles/panels/PanelList/index.js b/context/app/static/js/shared-styles/panels/PanelList/index.js
new file mode 100644
index 0000000000..f43f081dc7
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelList/index.js
@@ -0,0 +1,3 @@
+import PanelList from './PanelList';
+
+export default PanelList;
diff --git a/context/app/static/js/shared-styles/panels/PanelList/style.js b/context/app/static/js/shared-styles/panels/PanelList/style.js
new file mode 100644
index 0000000000..352d80b0e4
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelList/style.js
@@ -0,0 +1,12 @@
+import styled from 'styled-components';
+import Paper from '@material-ui/core/Paper';
+
+const PanelScrollBox = styled(Paper)`
+ @media (min-width: ${(props) => props.theme.breakpoints.values.md}px) {
+ flex-grow: 1;
+ overflow-y: scroll;
+ margin-top: ${(props) => props.theme.spacing(1)}px;
+ }
+`;
+
+export { PanelScrollBox };
diff --git a/context/app/static/js/shared-styles/panels/PanelListLandingPage/PanelListLandingPage.jsx b/context/app/static/js/shared-styles/panels/PanelListLandingPage/PanelListLandingPage.jsx
new file mode 100644
index 0000000000..7140765fe0
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelListLandingPage/PanelListLandingPage.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import Typography from '@material-ui/core/Typography';
+
+import PanelList from 'js/shared-styles/panels/PanelList';
+import { PageWrapper, StyledDescription } from './style';
+
+function PanelListLandingPage({ title, subtitle, description, panelsProps }) {
+ return (
+
+
+ {title}
+
+
+ {subtitle}
+
+ {description}
+ {panelsProps.length > 0 && }
+
+ );
+}
+
+export default PanelListLandingPage;
diff --git a/context/app/static/js/shared-styles/panels/PanelListLandingPage/PanelListLandingPage.stories.js b/context/app/static/js/shared-styles/panels/PanelListLandingPage/PanelListLandingPage.stories.js
new file mode 100644
index 0000000000..65a62bf314
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelListLandingPage/PanelListLandingPage.stories.js
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import Panel from 'js/shared-styles/panels/Panel';
+import PanelList from 'js/shared-styles/panels/PanelList';
+import { PanelList as PanelListStory } from 'js/shared-styles/panels/PanelList/PanelList.stories';
+import PanelListLandingPageComponent from './PanelListLandingPage';
+
+export default {
+ title: 'Panels/PanelListLandingPage',
+ component: PanelListLandingPageComponent,
+ subcomponents: [PanelList, Panel],
+};
+
+const lorem =
+ 'Fugiat irure nisi ea dolore non adipisicing non. Enim enim incididunt ut reprehenderit esse sint adipisicing. Aliqua excepteur reprehenderit tempor commodo anim veniam laboris labore exercitation qui. Adipisicing pariatur est anim nisi cupidatat ea Lorem nostrud labore laborum enim eiusmod.';
+
+export const PanelListLandingPage = (args) => ;
+
+PanelListLandingPage.args = {
+ title: 'Landing Page Title',
+ subtitle: 'Landing Page Subtitle',
+ description: lorem,
+ panelsProps: PanelListStory.args.panelsProps,
+};
+
+PanelListLandingPage.storyName = 'PanelListLandingPage'; // needed for single story hoisting for multi word component names
diff --git a/context/app/static/js/shared-styles/panels/PanelListLandingPage/index.js b/context/app/static/js/shared-styles/panels/PanelListLandingPage/index.js
new file mode 100644
index 0000000000..36e0b75e15
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelListLandingPage/index.js
@@ -0,0 +1,3 @@
+import PanelListLandingPage from './PanelListLandingPage';
+
+export default PanelListLandingPage;
diff --git a/context/app/static/js/shared-styles/panels/PanelListLandingPage/style.js b/context/app/static/js/shared-styles/panels/PanelListLandingPage/style.js
new file mode 100644
index 0000000000..ba1cc29588
--- /dev/null
+++ b/context/app/static/js/shared-styles/panels/PanelListLandingPage/style.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components';
+
+import Description from 'js/shared-styles/sections/Description';
+import { headerHeight } from 'js/components/Header/HeaderAppBar/style';
+
+// 88px = header height + header margin
+const PageWrapper = styled.div`
+ margin-bottom: ${(props) => props.theme.spacing(5)}px;
+ @media (min-width: ${(props) => props.theme.breakpoints.values.md}px) {
+ height: calc(100vh - ${headerHeight + 24}px);
+ display: flex;
+ flex-direction: column;
+ }
+`;
+
+const StyledDescription = styled(Description)`
+ margin-bottom: ${(props) => props.theme.spacing(2)}px;
+`;
+
+export { PageWrapper, StyledDescription };