diff --git a/frontend/src/metabase/collections/components/CollectionsList.jsx b/frontend/src/metabase/collections/components/CollectionsList.jsx deleted file mode 100644 index 19f5be759070599425d21e2e6312008391a24ceb..0000000000000000000000000000000000000000 --- a/frontend/src/metabase/collections/components/CollectionsList.jsx +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable react/prop-types */ -import React from "react"; -import { Box, Flex } from "grid-styled"; - -import * as Urls from "metabase/lib/urls"; - -import Icon from "metabase/components/Icon"; - -import CollectionLink from "metabase/collections/components/CollectionLink"; -import CollectionDropTarget from "metabase/containers/dnd/CollectionDropTarget"; -import { SIDEBAR_SPACER } from "metabase/collections/constants"; - -import { PLUGIN_COLLECTIONS } from "metabase/plugins"; - -import { CollectionListIcon } from "./CollectionsList.styled"; - -const { isRegularCollection } = PLUGIN_COLLECTIONS; - -class CollectionsList extends React.Component { - render() { - const { - currentCollection, - filter = () => true, - openCollections, - } = this.props; - const collections = this.props.collections.filter(filter); - - return ( - <Box> - {collections.map(c => { - const isOpen = openCollections.indexOf(c.id) >= 0; - const action = isOpen ? this.props.onClose : this.props.onOpen; - const hasChildren = - Array.isArray(c.children) && - c.children.some(child => !child.archived); - return ( - <Box key={c.id}> - <CollectionDropTarget collection={c}> - {({ highlighted, hovered }) => { - return ( - <CollectionLink - to={Urls.collection(c)} - selected={c.id === currentCollection} - depth={this.props.depth} - // when we click on a link, if there are children, expand to show sub collections - onClick={() => c.children && action(c.id)} - dimmedIcon={isRegularCollection(c)} - hovered={hovered} - highlighted={highlighted} - role="treeitem" - aria-expanded={isOpen} - > - <Flex - className="relative" - align={ - // if a collection name is somewhat long, align things at flex-start ("top") for a slightly better - // visual - c.name.length > 25 ? "flex-start" : "center" - } - > - {hasChildren && ( - <Flex - className="absolute text-brand cursor-pointer" - align="center" - justifyContent="center" - style={{ left: -20 }} - > - <Icon - name={isOpen ? "chevrondown" : "chevronright"} - onClick={ev => { - ev.preventDefault(); - action(c.id); - }} - size={12} - /> - </Flex> - )} - <CollectionListIcon collection={c} /> - {c.name} - </Flex> - </CollectionLink> - ); - }} - </CollectionDropTarget> - {c.children && isOpen && ( - <Box ml={-SIDEBAR_SPACER} pl={SIDEBAR_SPACER + 10}> - <CollectionsList - openCollections={openCollections} - onOpen={this.props.onOpen} - onClose={this.props.onClose} - collections={c.children} - filter={filter} - currentCollection={currentCollection} - depth={this.props.depth + 1} - /> - </Box> - )} - </Box> - ); - })} - </Box> - ); - } -} - -CollectionsList.defaultProps = { - depth: 1, -}; - -CollectionsList.Icon = CollectionListIcon; - -export default CollectionsList; diff --git a/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebar.styled.js b/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebar.styled.js index 2069e129ad8cad9fe14120d3fa06b4f8756b4413..c5d0fe98341b52183bd62d5816c70bda26f22cbc 100644 --- a/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebar.styled.js +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebar.styled.js @@ -37,6 +37,11 @@ export const Sidebar = styled(Box.withComponent("aside"))` box-shadow: 5px 0px 8px rgba(0, 0, 0, 0.35), 40px 0px rgba(5, 14, 31, 0.32); width: calc(100vw - 40px); + + ${breakpointMinSmall} { + box-shadow: none; + width: ${SIDEBAR_WIDTH}; + } `} ${breakpointMinSmall} { diff --git a/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebarFooter/CollectionSidebarFooter.jsx b/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebarFooter/CollectionSidebarFooter.jsx index e95f75b796f0d2d16cf8b0d09d528cbc7219abfc..716a569f008f856377b2a068814d3f74881e614a 100644 --- a/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebarFooter/CollectionSidebarFooter.jsx +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/CollectionSidebarFooter/CollectionSidebarFooter.jsx @@ -4,7 +4,7 @@ import { t } from "ttag"; import * as Urls from "metabase/lib/urls"; import { PERSONAL_COLLECTIONS } from "metabase/entities/collections"; -import CollectionsList from "metabase/collections/components/CollectionsList"; +import CollectionsList from "../Collections/CollectionsList/CollectionsList"; import { Container, Icon, Link } from "./CollectionSidebarFooter.styled"; const propTypes = { diff --git a/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/Collections.jsx b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/Collections.jsx index b10b1624470164f846d93b0a795e42737a6a182b..059f93f7ab179e31b82877af03fd35a59adf42c7 100644 --- a/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/Collections.jsx +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/Collections.jsx @@ -1,6 +1,6 @@ import React from "react"; import PropTypes from "prop-types"; -import CollectionsList from "metabase/collections/components/CollectionsList"; +import CollectionsList from "./CollectionsList/CollectionsList"; import { Box } from "grid-styled"; import { diff --git a/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.jsx b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.jsx new file mode 100644 index 0000000000000000000000000000000000000000..da6aa0914f1a49d54265429c1c617f61fe80c42c --- /dev/null +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.jsx @@ -0,0 +1,154 @@ +/* eslint-disable react/prop-types */ +import React from "react"; +import { Box } from "grid-styled"; + +import * as Urls from "metabase/lib/urls"; + +import Icon from "metabase/components/Icon"; +import { + CollectionListIcon, + ChildrenContainer, + ExpandCollectionButton, + LabelContainer, +} from "./CollectionsList.styled"; + +import CollectionLink from "metabase/collections/components/CollectionLink"; +import CollectionDropTarget from "metabase/containers/dnd/CollectionDropTarget"; + +import { PLUGIN_COLLECTIONS } from "metabase/plugins"; + +const { isRegularCollection } = PLUGIN_COLLECTIONS; + +function ToggleChildCollectionButton({ action, collectionId, isOpen }) { + const iconName = isOpen ? "chevrondown" : "chevronright"; + + function handleClick(e) { + e.preventDefault(); + action(collectionId); + } + + return ( + <ExpandCollectionButton> + <Icon name={iconName} onClick={handleClick} size={12} /> + </ExpandCollectionButton> + ); +} + +function Label({ action, collection, initialIcon, isOpen }) { + const { children, id, name } = collection; + + const hasChildren = + Array.isArray(children) && children.some(child => !child.archived); + + return ( + <LabelContainer> + {hasChildren && ( + <ToggleChildCollectionButton + action={action} + collectionId={id} + isOpen={isOpen} + /> + )} + + <CollectionListIcon collection={collection} /> + {name} + </LabelContainer> + ); +} + +function Collection({ + collection, + depth, + currentCollection, + filter, + initialIcon, + onClose, + onOpen, + openCollections, +}) { + const { id, children } = collection; + const isOpen = openCollections.indexOf(id) >= 0; + const action = isOpen ? onClose : onOpen; + + return ( + <Box> + <CollectionDropTarget collection={collection}> + {({ highlighted, hovered }) => { + const url = Urls.collection(collection); + const selected = id === currentCollection; + const dimmedIcon = isRegularCollection(collection); + + // when we click on a link, if there are children, + // expand to show sub collections + function handleClick() { + children && action(id); + } + + return ( + <CollectionLink + to={url} + selected={selected} + depth={depth} + onClick={handleClick} + dimmedIcon={dimmedIcon} + hovered={hovered} + highlighted={highlighted} + role="treeitem" + aria-expanded={isOpen} + > + <Label + action={action} + collection={collection} + initialIcon={initialIcon} + isOpen={isOpen} + /> + </CollectionLink> + ); + }} + </CollectionDropTarget> + + {children && isOpen && ( + <ChildrenContainer> + <CollectionsList + openCollections={openCollections} + onOpen={onOpen} + onClose={onClose} + collections={children} + filter={filter} + currentCollection={currentCollection} + depth={depth + 1} + /> + </ChildrenContainer> + )} + </Box> + ); +} + +function CollectionsList({ + collections, + filter, + initialIcon, + depth = 1, + ...otherProps +}) { + const filteredCollections = collections.filter(filter); + + return ( + <Box> + {filteredCollections.map(collection => ( + <Collection + collection={collection} + depth={depth} + filter={filter} + initialIcon={initialIcon} + key={collection.id} + {...otherProps} + /> + ))} + </Box> + ); +} + +CollectionsList.Icon = CollectionListIcon; + +export default CollectionsList; diff --git a/frontend/src/metabase/collections/components/CollectionsList.styled.js b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.styled.js similarity index 52% rename from frontend/src/metabase/collections/components/CollectionsList.styled.js rename to frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.styled.js index 1342d257dc0e764cbd4a70c0df1a7744e8f099c9..a91123226d2b2bebfc68827f56cef8cef3c5bae6 100644 --- a/frontend/src/metabase/collections/components/CollectionsList.styled.js +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.styled.js @@ -8,6 +8,11 @@ import { CollectionIcon } from "metabase/collections/components/CollectionIcon"; const { isRegularCollection } = PLUGIN_COLLECTIONS; +import { SIDEBAR_SPACER } from "metabase/collections/constants"; +import { color } from "metabase/lib/colors"; + +import IconButtonWrapper from "metabase/components/IconButtonWrapper"; + function getOpacity(collection) { if ( collection.id === ROOT_COLLECTION.id || @@ -22,3 +27,22 @@ export const CollectionListIcon = styled(CollectionIcon)` margin-right: 6px; opacity: ${props => getOpacity(props.collection)}; `; + +export const ChildrenContainer = styled.div` + box-sizing: border-box; + margin-left: -${SIDEBAR_SPACER}px; + padding-left: ${SIDEBAR_SPACER + 10}px; +`; + +export const ExpandCollectionButton = styled(IconButtonWrapper)` + align-items: center; + color: ${color("brand")}; + cursor: pointer; + left: -20px; + position: absolute; +`; + +export const LabelContainer = styled.div` + display: flex; + position: relative; +`; diff --git a/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.unit.spec.js b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.unit.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6c595172ee1914cba8a310548112adc0d7fbc2b2 --- /dev/null +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/Collections/CollectionsList/CollectionsList.unit.spec.js @@ -0,0 +1,72 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { DragDropContextProvider } from "react-dnd"; +import HTML5Backend from "react-dnd-html5-backend"; + +import CollectionsList from "./CollectionsList"; + +const filter = () => true; + +const openCollections = []; + +it("renders a basic collection", () => { + const collections = [ + { + archived: false, + children: [], + id: 1, + location: "/", + name: "Collection name", + }, + ]; + + render( + <DragDropContextProvider backend={HTML5Backend}> + <CollectionsList + collections={collections} + filter={filter} + openCollections={openCollections} + /> + </DragDropContextProvider>, + ); + + screen.getByText("Collection name"); +}); + +it("opens child collection when user clicks on chevron button", () => { + const parentCollection = { + archived: false, + children: [], + id: 1, + location: "/", + name: "Parent collection name", + }; + + const childCollection = { + archived: false, + children: [], + id: 2, + location: "/2/", + name: "Child collection name", + }; + + parentCollection.children = [childCollection]; + + const onOpen = jest.fn(); + + render( + <DragDropContextProvider backend={HTML5Backend}> + <CollectionsList + collections={[parentCollection]} + filter={filter} + onOpen={onOpen} + openCollections={openCollections} + /> + </DragDropContextProvider>, + ); + + const chevronButton = screen.getByLabelText("chevronright icon"); + fireEvent.click(chevronButton); + + expect(onOpen).toHaveBeenCalled(); +}); diff --git a/frontend/src/metabase/collections/containers/CollectionSidebar/RootCollectionLink/RootCollectionLink.jsx b/frontend/src/metabase/collections/containers/CollectionSidebar/RootCollectionLink/RootCollectionLink.jsx index f248510e7ffc3c59bc8872f4296506f475cec6ff..9af0e65bb657a354c78ff4b1681a0be215dac4a2 100644 --- a/frontend/src/metabase/collections/containers/CollectionSidebar/RootCollectionLink/RootCollectionLink.jsx +++ b/frontend/src/metabase/collections/containers/CollectionSidebar/RootCollectionLink/RootCollectionLink.jsx @@ -5,7 +5,7 @@ import { t } from "ttag"; import * as Urls from "metabase/lib/urls"; import CollectionDropTarget from "metabase/containers/dnd/CollectionDropTarget"; -import CollectionsList from "metabase/collections/components/CollectionsList"; +import CollectionsList from "../Collections/CollectionsList/CollectionsList"; import CollectionLink from "metabase/collections/components/CollectionLink"; import { Container } from "./RootCollectionLink.styled"; diff --git a/frontend/src/metabase/components/EntityItem.styled.js b/frontend/src/metabase/components/EntityItem.styled.js index bc58a655b288b445541557b848904c6f88189662..d9e775b10ec3ba0aad486144165b686625512919 100644 --- a/frontend/src/metabase/components/EntityItem.styled.js +++ b/frontend/src/metabase/components/EntityItem.styled.js @@ -24,6 +24,7 @@ function getForeground(model) { } export const EntityIconWrapper = styled(IconButtonWrapper)` + background-color: ${color("bg-medium")}; padding: 12px; color: ${props => diff --git a/frontend/src/metabase/components/IconButtonWrapper.jsx b/frontend/src/metabase/components/IconButtonWrapper.jsx index 0ab43ddf085283a4053a0a5a192548f0d49df44a..bc96b25c3110efface9b047e36a7d1cd5420fe17 100644 --- a/frontend/src/metabase/components/IconButtonWrapper.jsx +++ b/frontend/src/metabase/components/IconButtonWrapper.jsx @@ -1,12 +1,9 @@ import styled from "styled-components"; -import { color } from "metabase/lib/colors"; - const IconButtonWrapper = styled.button.attrs({ type: "button" })` display: flex; align-items: center; justify-content: center; - background-color: ${color("bg-medium")}; border-radius: ${props => (props.circle ? "50%" : "6px")}; `;