Skip to content
Snippets Groups Projects
Commit 32a88aa4 authored by Anton Kulyk's avatar Anton Kulyk Committed by Reza Lotun
Browse files

Display official badge throughout the application (#17125)

* Add getCollectionIcon function

* Add isRegularCollection function

* Display official badge in collections sidebar

* Don't dim official badge icon in collections sidebar

* Show root collection icon

* Use `CollectionsList.Icon` for "All personal collections"

* Fix CollectionBadge icon color

* Display official collection badge in search results

* Test official badge displayed correctly in sidebar

* Test official badge is shown throughout the app

* Show official badge icon in SavedQuestionPicker

* Use existing isPersonalCollection utility

* Show official badge icon in QuestionPicker

* Show official badge icon in CollectionPicker

* Minor CollectionIcon refactoring

* Use function declaration for isRegularCollection

* Add editCollection test helper

* Add changeCollectionTypeTo helper

* Remove duplicated assertions

* Add helper to create official collection

* Add testOfficialBadgeInSearch helper
parent 0a687782
No related branches found
No related tags found
No related merge requests found
Showing
with 189 additions and 57 deletions
......@@ -12,7 +12,15 @@ import {
OFFICIAL_COLLECTION,
} from "./constants";
function isRegularCollection({ authority_level }) {
// Root, personal collections don't have `authority_level`
return !authority_level || authority_level === REGULAR_COLLECTION.type;
}
PLUGIN_COLLECTIONS.isRegularCollection = isRegularCollection;
PLUGIN_COLLECTIONS.REGULAR_COLLECTION = REGULAR_COLLECTION;
PLUGIN_COLLECTIONS.AUTHORITY_LEVEL = AUTHORITY_LEVELS;
PLUGIN_COLLECTIONS.formFields = [
......
import React from "react";
import PropTypes from "prop-types";
import Icon from "metabase/components/Icon";
import { getCollectionIcon } from "metabase/entities/collections";
const propTypes = {
collection: PropTypes.shape({
authority_level: PropTypes.oneOf(["official"]),
}),
};
export function CollectionIcon({ collection, ...props }) {
const { name, color } = getCollectionIcon(collection);
return <Icon name={name} color={color} {...props} />;
}
CollectionIcon.propTypes = propTypes;
import styled from "styled-components";
import styled, { css } from "styled-components";
import Link from "metabase/components/Link";
import { color } from "metabase/lib/colors";
import { SIDEBAR_SPACER } from "../constants";
const dimmedIconCss = css`
fill: ${color("white")};
opacity: 0.8;
`;
const CollectionLink = styled(Link)`
margin-left: ${props =>
// use negative margin to reset our potentially nested item back by the depth
......@@ -34,9 +39,14 @@ const CollectionLink = styled(Link)`
? color("brand")
: color("bg-medium")};
}
.Icon {
fill: ${props => props.selected && "white"};
opacity: ${props => props.selected && "0.8"};
${props => props.selected && props.dimmedIcon && dimmedIconCss}
}
.Icon-chevronright,
.Icon-chevrondown {
${props => props.selected && dimmedIconCss}
}
`;
......
import styled from "styled-components";
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import {
ROOT_COLLECTION,
PERSONAL_COLLECTIONS,
} from "metabase/entities/collections";
import { CollectionIcon } from "metabase/collections/components/CollectionIcon";
const { isRegularCollection } = PLUGIN_COLLECTIONS;
function getOpacity(collection) {
if (
collection.id === ROOT_COLLECTION.id ||
collection.id === PERSONAL_COLLECTIONS.id
) {
return 1;
}
return isRegularCollection(collection) ? 0.4 : 1;
}
export const CollectionListIcon = styled(CollectionIcon)`
margin-right: 6px;
opacity: ${props => getOpacity(props.collection)};
`;
......@@ -10,10 +10,15 @@ 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 "./CollectionList.styled";
const { isRegularCollection } = PLUGIN_COLLECTIONS;
class CollectionsList extends React.Component {
render() {
const {
initialIcon,
currentCollection,
filter = () => true,
openCollections,
......@@ -39,6 +44,7 @@ class CollectionsList extends React.Component {
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"
......@@ -69,11 +75,7 @@ class CollectionsList extends React.Component {
/>
</Flex>
)}
<Icon
name={initialIcon}
mr={"6px"}
style={{ opacity: 0.4 }}
/>
<CollectionListIcon collection={c} />
{c.name}
</Flex>
</CollectionLink>
......@@ -102,8 +104,9 @@ class CollectionsList extends React.Component {
}
CollectionsList.defaultProps = {
initialIcon: "folder",
depth: 1,
};
CollectionsList.Icon = CollectionListIcon;
export default CollectionsList;
......@@ -3,6 +3,8 @@ import PropTypes from "prop-types";
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 { Container, Icon, Link } from "./CollectionSidebarFooter.styled";
const propTypes = {
......@@ -14,7 +16,7 @@ export default function CollectionSidebarFooter({ isAdmin }) {
<Container>
{isAdmin && (
<Link to={Urls.collection({ id: "users" })}>
<Icon name="group" />
<CollectionsList.Icon collection={PERSONAL_COLLECTIONS} />
{t`Other users' personal collections`}
</Link>
)}
......
......@@ -53,7 +53,6 @@ export default function Collections({
onClose={onClose}
onOpen={onOpen}
collections={currentUserPersonalCollections}
initialIcon="person"
filter={filterPersonalCollections}
currentCollection={collectionId}
/>
......
......@@ -5,6 +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 CollectionLink from "metabase/collections/components/CollectionLink";
import { Container } from "./RootCollectionLink.styled";
......@@ -25,6 +26,7 @@ export default function CollectionSidebarHeader({ isRoot, root }) {
highlighted={highlighted}
hovered={hovered}
>
<CollectionsList.Icon collection={root} />
{t`Our analytics`}
</CollectionLink>
)}
......
......@@ -6,7 +6,14 @@ import Icon from "metabase/components/Icon";
import cx from "classnames";
export default function Badge({ icon, name, className, children, ...props }) {
export default function Badge({
icon,
iconColor,
name,
className,
children,
...props
}) {
return (
<MaybeLink
className={cx(
......@@ -18,7 +25,14 @@ export default function Badge({ icon, name, className, children, ...props }) {
)}
{...props}
>
{icon && <Icon name={icon} mr={children ? "5px" : null} size={11} />}
{icon && (
<Icon
name={icon}
mr={children ? "5px" : null}
color={iconColor}
size={12}
/>
)}
{children && <span className="text-wrap">{children}</span>}
</MaybeLink>
);
......
......@@ -8,6 +8,7 @@ const propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
icon: PropTypes.string.isRequired,
iconColor: PropTypes.string,
onSelect: PropTypes.func.isRequired,
isSelected: PropTypes.bool,
isHighlighted: PropTypes.bool,
......@@ -19,6 +20,7 @@ export function SelectListItem({
id,
name,
icon,
iconColor = "brand",
onSelect,
isSelected = false,
isHighlighted = false,
......@@ -37,7 +39,7 @@ export function SelectListItem({
onClick={() => onSelect(id)}
onKeyDown={e => e.key === "Enter" && onSelect(id)}
>
<ItemIcon name={icon} isHighlighted={isHighlighted} />
<ItemIcon name={icon} color={iconColor} isHighlighted={isHighlighted} />
<ItemTitle>{name}</ItemTitle>
{hasRightArrow && <ItemIcon name="chevronright" />}
</ItemRoot>
......
import styled, { css } from "styled-components";
import Label from "metabase/components/type/Label";
import colors from "metabase/lib/colors";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
export const ItemTitle = styled(Label)`
......@@ -12,15 +12,15 @@ export const ItemTitle = styled(Label)`
export const ItemIcon = styled(Icon)`
color: ${props =>
props.isHighlighted ? colors["brand"] : colors["text-light"]};
props.isHighlighted ? color(props.color) : color("text-light")};
`;
const activeItemCss = css`
background-color: ${colors["brand"]};
background-color: ${color("brand")};
${ItemIcon},
${ItemTitle} {
color: ${colors["white"]};
color: ${color("white")};
}
`;
......
......@@ -21,6 +21,7 @@ const propTypes = {
item: PropTypes.shape({
name: PropTypes.string.isRequired,
icon: PropTypes.string,
iconColor: PropTypes.string,
hasRightArrow: PropTypes.string,
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
}).isRequired,
......@@ -42,7 +43,7 @@ export const TreeNode = React.memo(
},
ref,
) {
const { name, icon, hasRightArrow, id } = item;
const { name, icon, iconColor, hasRightArrow, id } = item;
const handleSelect = () => {
onSelect(item);
......@@ -80,7 +81,7 @@ export const TreeNode = React.memo(
{icon && (
<IconContainer variant={variant}>
<Icon name={icon} />
<Icon name={icon} color={iconColor} />
</IconContainer>
)}
<NameContainer>{name}</NameContainer>
......
......@@ -199,14 +199,17 @@ export default class ItemPicker extends React.Component {
// NOTE: this assumes the only reason you'd be selecting a collection is to modify it in some way
const canSelect =
models.has("collection") && collection.can_write;
const icon = getCollectionIcon(collection);
// only show if collection can be selected or has children
return canSelect || hasChildren ? (
<Item
key={`collection-${collection.id}`}
item={collection}
name={collection.name}
color={COLLECTION_ICON_COLOR}
icon={getCollectionIcon(collection).name}
color={color(icon.color) || COLLECTION_ICON_COLOR}
icon={icon.name}
selected={canSelect && isSelected(collection)}
canSelect={canSelect}
hasChildren={hasChildren}
......
......@@ -11,12 +11,16 @@ import { entityListLoader } from "metabase/entities/containers/EntityListLoader"
import Collections, { ROOT_COLLECTION } from "metabase/entities/collections";
import { useDebouncedValue } from "metabase/hooks/use-debounced-value";
import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import { QuestionList } from "./QuestionList";
import { BreadcrumbsWrapper, SearchInput } from "./QuestionPicker.styled";
import { SEARCH_DEBOUNCE_DURATION } from "metabase/lib/constants";
import { SelectList } from "metabase/components/select-list";
const { isRegularCollection } = PLUGIN_COLLECTIONS;
QuestionPicker.propTypes = {
onSelect: PropTypes.func.isRequired,
collectionsById: PropTypes.object,
......@@ -78,16 +82,23 @@ function QuestionPicker({
</BreadcrumbsWrapper>
<SelectList>
{collections.map(collection => (
<SelectList.Item
hasRightArrow
key={collection.id}
id={collection.id}
name={collection.name}
icon={getCollectionIcon(collection).name}
onSelect={collectionId => setCurrentCollectionId(collectionId)}
/>
))}
{collections.map(collection => {
const icon = getCollectionIcon(collection);
return (
<SelectList.Item
hasRightArrow
key={collection.id}
id={collection.id}
name={collection.name}
icon={icon.name}
iconColor={icon.color}
isHighlighted={!isRegularCollection(collection)}
onSelect={collectionId =>
setCurrentCollectionId(collectionId)
}
/>
);
})}
</SelectList>
</React.Fragment>
)}
......
......@@ -9,6 +9,7 @@ import { createSelector } from "reselect";
import { GET } from "metabase/lib/api";
import { getUser, getUserPersonalCollectionId } from "metabase/selectors/user";
import { isPersonalCollection } from "metabase/collections/utils";
import { t } from "ttag";
......@@ -81,7 +82,7 @@ const Collections = createEntity({
objectSelectors: {
getName: collection => collection && collection.name,
getUrl: collection => Urls.collection(collection),
getIcon: collection => ({ name: "folder" }),
getIcon: getCollectionIcon,
},
selectors: {
......@@ -172,6 +173,20 @@ const Collections = createEntity({
export default Collections;
export function getCollectionIcon(collection) {
if (collection.id === PERSONAL_COLLECTIONS.id) {
return { name: "group" };
}
if (isPersonalCollection(collection)) {
return { name: "person" };
}
const authorityLevel =
PLUGIN_COLLECTIONS.AUTHORITY_LEVEL[collection.authority_level];
return authorityLevel
? { name: authorityLevel.icon, color: color(authorityLevel.color) }
: { name: "folder" };
}
// API requires items in "root" collection be persisted with a "null" collection ID
// Also ensure it's parsed as a number
export const canonicalCollectionId = (
......
......@@ -158,7 +158,11 @@ export default class SearchBar extends React.Component {
debounced
>
{({ list }) => {
return <ol>{this.renderResults(list)}</ol>;
return (
<ol data-testid="search-results-list">
{this.renderResults(list)}
</ol>
);
}}
</Search.ListLoader>
</Card>
......
......@@ -66,6 +66,7 @@ const AUTHORITY_LEVEL_REGULAR = {
export const PLUGIN_COLLECTIONS = {
formFields: [],
isRegularCollection: () => true,
REGULAR_COLLECTION: AUTHORITY_LEVEL_REGULAR,
AUTHORITY_LEVEL: {
[AUTHORITY_LEVEL_REGULAR.type]: AUTHORITY_LEVEL_REGULAR,
......
import { isPersonalCollection } from "metabase/collections/utils";
import { PERSONAL_COLLECTIONS } from "metabase/entities/collections";
const getCollectionIcon = collection => {
if (collection.id === PERSONAL_COLLECTIONS.id) {
return "group";
}
return isPersonalCollection(collection) ? "person" : "folder";
};
import { getCollectionIcon } from "metabase/entities/collections";
export function buildCollectionTree(collections) {
if (collections == null) {
return [];
}
return collections.map(collection => ({
id: collection.id,
name: collection.name,
schemaName: collection.originalName || collection.name,
icon: getCollectionIcon(collection),
children: buildCollectionTree(collection.children),
}));
return collections.map(collection => {
const icon = getCollectionIcon(collection);
return {
id: collection.id,
name: collection.name,
schemaName: collection.originalName || collection.name,
icon: icon.name,
iconColor: icon.color,
children: buildCollectionTree(collection.children),
};
});
}
export const findCollectionByName = (collections, name) => {
......
......@@ -17,10 +17,12 @@ class CollectionBadge extends React.Component {
if (!collection) {
return null;
}
const icon = collection.getIcon();
return (
<Badge
to={collection.getUrl()}
icon={collection.getIcon().name}
icon={icon.name}
iconColor={icon.color}
data-metabase-event={`${analyticsContext};Collection Badge Click`}
{...props}
>
......
......@@ -12,10 +12,14 @@ import Icon from "metabase/components/Icon";
import Link from "metabase/components/Link";
import Text from "metabase/components/type/Text";
import { PLUGIN_COLLECTION_COMPONENTS } from "metabase/plugins";
import Schema from "metabase/entities/schemas";
import Database from "metabase/entities/databases";
import Table from "metabase/entities/tables";
const { CollectionAuthorityLevelIcon } = PLUGIN_COLLECTION_COMPONENTS;
function getColorForIconWrapper(props) {
if (props.item.collection_position) {
return color("saturated-yellow");
......@@ -97,18 +101,33 @@ function ItemIcon({ item, type }) {
);
}
const CollectionBadgeRoot = styled.div`
display: inline-block;
`;
const CollectionLink = styled(Link)`
display: flex;
align-items: center;
text-decoration: dashed;
&:hover {
color: ${color("brand")};
}
`;
const AuthorityLevelIcon = styled(CollectionAuthorityLevelIcon).attrs({
size: 13,
})`
padding-right: 2px;
`;
function CollectionBadge({ collection }) {
return (
<CollectionLink to={Urls.collection(collection)}>
{collection.name}
</CollectionLink>
<CollectionBadgeRoot>
<CollectionLink to={Urls.collection(collection)}>
<AuthorityLevelIcon collection={collection} />
{collection.name}
</CollectionLink>
</CollectionBadgeRoot>
);
}
......
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