Skip to content
Snippets Groups Projects
Unverified Commit 1c5d4146 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Select collections for selected questions, models, and dashboards in the sidebar (#23725)

parent 5965c3a0
No related branches found
No related tags found
No related merge requests found
......@@ -7,6 +7,7 @@ import { CardId, SavedCard } from "metabase-types/types/Card";
export interface Dashboard {
id: number;
collection_id: number | null;
name: string;
description: string | null;
model?: string;
......
......@@ -2,6 +2,7 @@ import { Dashboard } from "metabase-types/api";
export const createMockDashboard = (opts?: Partial<Dashboard>): Dashboard => ({
id: 1,
collection_id: null,
name: "Dashboard",
ordered_cards: [],
can_write: true,
......
......@@ -92,6 +92,12 @@ export function isFullyParametrized(item: Item) {
return item.fully_parametrized ?? true;
}
export function coerceCollectionId(
collectionId: number | null | undefined,
): string | number {
return collectionId == null ? "root" : collectionId;
}
// API requires items in "root" collection be persisted with a "null" collection ID
// Also ensure it's parsed as a number
export function canonicalCollectionId(
......
......@@ -24,7 +24,7 @@ const mapStateToProps = (state: State, props: RouterProps) => ({
isNewButtonVisible: getIsNewButtonVisible(state),
isProfileLinkVisible: getIsProfileLinkVisible(state),
isCollectionPathVisible: getIsCollectionPathVisible(state, props),
isQuestionLineageVisible: getIsQuestionLineageVisible(state),
isQuestionLineageVisible: getIsQuestionLineageVisible(state, props),
});
const mapDispatchToProps = {
......
import React, { useCallback, useEffect, useState } from "react";
import { t } from "ttag";
import { connect } from "react-redux";
import _ from "underscore";
import "./sortable.css";
......@@ -18,7 +17,7 @@ import { PLUGIN_COLLECTIONS } from "metabase/plugins";
import Bookmarks from "metabase/entities/bookmarks";
import * as Urls from "metabase/lib/urls";
import { SelectedEntityItem } from "../types";
import { SelectedItem } from "../types";
import { SidebarHeading } from "../MainNavbar.styled";
import { DragIcon, SidebarBookmarkItem } from "./BookmarkList.styled";
......@@ -29,7 +28,7 @@ const mapDispatchToProps = {
interface CollectionSidebarBookmarksProps {
bookmarks: BookmarksType;
selectedItem?: SelectedEntityItem;
selectedItem?: SelectedItem;
onSelect: () => void;
onDeleteBookmark: (bookmark: Bookmark) => void;
reorderBookmarks: ({
......@@ -45,7 +44,7 @@ interface BookmarkItemProps {
bookmark: Bookmark;
index: number;
isSorting: boolean;
selectedItem?: SelectedEntityItem;
selectedItem?: SelectedItem;
onSelect: () => void;
onDeleteBookmark: (bookmark: Bookmark) => void;
}
......
......@@ -9,7 +9,14 @@ import { IconProps } from "metabase/components/Icon";
import Modal from "metabase/components/Modal";
import LoadingSpinner from "metabase/components/LoadingSpinner";
import { Bookmark, BookmarksType, Collection, User } from "metabase-types/api";
import Question from "metabase-lib/lib/Question";
import {
Bookmark,
BookmarksType,
Collection,
Dashboard,
User,
} from "metabase-types/api";
import { State } from "metabase-types/store";
import Bookmarks, { getOrderedBookmarks } from "metabase/entities/bookmarks";
......@@ -25,10 +32,13 @@ import {
getHasOwnDatabase,
getHasDataAccess,
} from "metabase/new_query/selectors";
import { getQuestion } from "metabase/query_builder/selectors";
import { getDashboard } from "metabase/dashboard/selectors";
import {
nonPersonalOrArchivedCollection,
currentUserPersonalCollections,
coerceCollectionId,
} from "metabase/collections/utils";
import * as Urls from "metabase/lib/urls";
......@@ -52,6 +62,8 @@ function mapStateToProps(state: State) {
hasDataAccess: getHasDataAccess(state),
hasOwnDatabase: getHasOwnDatabase(state),
bookmarks: getOrderedBookmarks(state),
question: getQuestion(state),
dashboard: getDashboard(state),
};
}
......@@ -75,6 +87,8 @@ type Props = {
bookmarks: BookmarksType;
collections: Collection[];
rootCollection: Collection;
question?: Question;
dashboard?: Dashboard;
hasDataAccess: boolean;
hasOwnDatabase: boolean;
allFetched: boolean;
......@@ -99,6 +113,8 @@ function MainNavbarContainer({
hasOwnDatabase,
collections = [],
rootCollection,
question,
dashboard,
hasDataAccess,
allFetched,
location,
......@@ -129,23 +145,49 @@ function MainNavbarContainer({
};
}, [isOpen, openNavbar, closeNavbar]);
const selectedItem = useMemo<SelectedItem>(() => {
const selectedItems = useMemo<SelectedItem[]>(() => {
const { pathname } = location;
const { slug } = params;
if (pathname.startsWith("/collection")) {
const id = pathname.startsWith("/collection/users")
? "users"
: Urls.extractCollectionId(slug);
return { type: "collection", id };
const isCollectionPath = pathname.startsWith("/collection");
const isUsersCollectionPath = pathname.startsWith("/collection/users");
const isQuestionPath = pathname.startsWith("/question");
const isModelPath = pathname.startsWith("/model");
const isDashboardPath = pathname.startsWith("/dashboard");
if (isCollectionPath) {
return [
{
id: isUsersCollectionPath ? "users" : Urls.extractCollectionId(slug),
type: "collection",
},
];
}
if (pathname.startsWith("/dashboard")) {
return { type: "dashboard", id: Urls.extractEntityId(slug) };
if (isDashboardPath && dashboard) {
return [
{
id: dashboard.id,
type: "dashboard",
},
{
id: coerceCollectionId(dashboard.collection_id),
type: "collection",
},
];
}
if (pathname.startsWith("/question") || pathname.startsWith("/model")) {
return { type: "card", id: Urls.extractEntityId(slug) };
if ((isQuestionPath || isModelPath) && question) {
return [
{
id: question.id(),
type: "card",
},
{
id: coerceCollectionId(question.collectionId()),
type: "collection",
},
];
}
return { type: "non-entity", url: pathname };
}, [location, params]);
return [{ url: pathname, type: "non-entity" }];
}, [location, params, question, dashboard]);
const collectionTree = useMemo<CollectionTreeItem[]>(() => {
if (!rootCollection) {
......@@ -220,7 +262,7 @@ function MainNavbarContainer({
currentUser={currentUser}
collections={collectionTree}
hasOwnDatabase={hasOwnDatabase}
selectedItem={selectedItem}
selectedItems={selectedItems}
hasDataAccess={hasDataAccess}
reorderBookmarks={reorderBookmarks}
handleCreateNewCollection={onCreateNewCollection}
......
import React, { useCallback } from "react";
import { t } from "ttag";
import _ from "underscore";
import { BookmarksType, Collection, User } from "metabase-types/api";
......@@ -43,7 +44,7 @@ type Props = {
hasDataAccess: boolean;
hasOwnDatabase: boolean;
collections: CollectionTreeItem[];
selectedItem: SelectedItem;
selectedItems: SelectedItem[];
handleCloseNavbar: () => void;
handleLogout: () => void;
handleCreateNewCollection: () => void;
......@@ -67,15 +68,18 @@ function MainNavbarView({
bookmarks,
collections,
hasOwnDatabase,
selectedItem,
selectedItems,
hasDataAccess,
reorderBookmarks,
handleCreateNewCollection,
handleCloseNavbar,
}: Props) {
const isNonEntityLinkSelected = selectedItem.type === "non-entity";
const isCollectionSelected =
selectedItem.type === "collection" && selectedItem.id !== "users";
const {
card: cardItem,
collection: collectionItem,
dashboard: dashboardItem,
"non-entity": nonEntityItem,
} = _.indexBy(selectedItems, item => item.type);
const onItemSelect = useCallback(() => {
if (isSmallScreen()) {
......@@ -89,7 +93,7 @@ function MainNavbarView({
<SidebarSection>
<ul>
<HomePageLink
isSelected={isNonEntityLinkSelected && selectedItem.url === "/"}
isSelected={nonEntityItem?.url === "/"}
icon="home"
onClick={onItemSelect}
url="/"
......@@ -103,9 +107,7 @@ function MainNavbarView({
<SidebarSection>
<BookmarkList
bookmarks={bookmarks}
selectedItem={
selectedItem.type !== "non-entity" ? selectedItem : undefined
}
selectedItem={cardItem ?? dashboardItem ?? collectionItem}
onSelect={onItemSelect}
reorderBookmarks={reorderBookmarks}
/>
......@@ -119,7 +121,7 @@ function MainNavbarView({
/>
<Tree
data={collections}
selectedId={isCollectionSelected ? selectedItem.id : undefined}
selectedId={collectionItem?.id}
onSelect={onItemSelect}
TreeNode={SidebarCollectionLink}
role="tree"
......@@ -134,10 +136,7 @@ function MainNavbarView({
<BrowseLink
icon="database"
url={BROWSE_URL}
isSelected={
isNonEntityLinkSelected &&
selectedItem.url.startsWith(BROWSE_URL)
}
isSelected={nonEntityItem?.url?.startsWith(BROWSE_URL)}
onClick={onItemSelect}
data-metabase-event="NavBar;Data Browse"
>
......@@ -147,10 +146,9 @@ function MainNavbarView({
<AddYourOwnDataLink
icon="add"
url={ADD_YOUR_OWN_DATA_URL}
isSelected={
isNonEntityLinkSelected &&
selectedItem.url.startsWith(ADD_YOUR_OWN_DATA_URL)
}
isSelected={nonEntityItem?.url?.startsWith(
ADD_YOUR_OWN_DATA_URL,
)}
onClick={onItemSelect}
data-metabase-event="NavBar;Add your own data"
>
......
export type SelectedEntityItem = {
type: "card" | "collection" | "dashboard";
export interface SelectedItem {
type: "card" | "collection" | "dashboard" | "non-entity";
id?: number | string;
};
export type SelectedNonEntityItem = {
type: "non-entity";
url: string;
};
export type SelectedItem = SelectedEntityItem | SelectedNonEntityItem;
url?: string;
}
......@@ -29,6 +29,7 @@ const PATHS_WITH_COLLECTION_BREADCRUMBS = [
/\/model\//,
/\/dashboard\//,
];
const PATHS_WITH_QUESTION_LINEAGE = [/\/question/, /\/model/];
export const getRouterPath = (state: State, props: RouterProps) => {
return props.location.pathname;
......@@ -139,10 +140,11 @@ export const getIsCollectionPathVisible = createSelector(
);
export const getIsQuestionLineageVisible = createSelector(
[getQuestion, getOriginalQuestion],
(question, originalQuestion) =>
[getQuestion, getOriginalQuestion, getRouterPath],
(question, originalQuestion, path) =>
question != null &&
!question.isSaved() &&
originalQuestion != null &&
!originalQuestion.isDataset(),
!originalQuestion.isDataset() &&
PATHS_WITH_QUESTION_LINEAGE.some(pattern => pattern.test(path)),
);
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