Skip to content
Snippets Groups Projects
Unverified Commit 49b6954e authored by Gustavo Saiani's avatar Gustavo Saiani Committed by GitHub
Browse files

Improve CollectionsList component (#17277)

parent f4050ff1
No related merge requests found
Showing
with 259 additions and 118 deletions
/* 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;
......@@ -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} {
......
......@@ -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 = {
......
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 {
......
/* 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;
......@@ -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;
`;
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();
});
......@@ -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";
......
......@@ -24,6 +24,7 @@ function getForeground(model) {
}
export const EntityIconWrapper = styled(IconButtonWrapper)`
background-color: ${color("bg-medium")};
padding: 12px;
color: ${props =>
......
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")};
`;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment