From 0a986a58173001c138dcee58c2b6c2d3f79d8c03 Mon Sep 17 00:00:00 2001
From: Alexander Polyankin <alexander.polyankin@metabase.com>
Date: Thu, 23 Jun 2022 18:07:41 +0300
Subject: [PATCH] Rework collection header actions (#23484)

---
 frontend/src/metabase-types/api/collection.ts |   1 +
 .../metabase-types/api/mocks/collection.ts    |   1 +
 .../CollectionEmptyState.tsx                  |  10 +-
 .../CollectionHeader/CollectionBookmark.tsx   |  42 +++++
 .../CollectionCaption.styled.tsx              |  21 +++
 .../CollectionHeader/CollectionCaption.tsx    |  35 ++++
 .../CollectionHeader/CollectionHeader.jsx     | 176 ------------------
 .../CollectionHeader.styled.jsx               |  85 ---------
 .../CollectionHeader.styled.tsx               |  22 +++
 .../CollectionHeader/CollectionHeader.tsx     |  47 +++++
 .../CollectionHeader.unit.spec.js             | 132 -------------
 .../CollectionHeader/CollectionMenu.tsx       |  68 +++++++
 .../CollectionHeader/CollectionTimeline.tsx   |  29 +++
 .../components/CollectionHeader/index.ts      |   1 +
 .../MoveCollectionModal.tsx                   |  34 ++++
 .../components/MoveCollectionModal/index.ts   |   1 +
 .../PinnedItemCard/PinnedItemCard.stories.tsx |   1 +
 .../containers/CollectionContent.jsx          |  21 ++-
 .../MoveCollectionModal.tsx                   |  28 +++
 .../containers/MoveCollectionModal/index.ts   |   1 +
 .../BookmarkToggle/BookmarkToggle.stories.tsx |  29 +++
 .../BookmarkToggle/BookmarkToggle.styled.tsx  |  31 +++
 .../BookmarkToggle/BookmarkToggle.tsx         |  65 +++++++
 .../core/components/BookmarkToggle/index.ts   |   1 +
 .../src/metabase/nav/containers/AppBar.tsx    |   4 +-
 frontend/src/metabase/routes.jsx              |   2 +
 .../e2e/helpers/e2e-collection-helpers.js     |  12 +-
 .../collections/collections.cy.spec.js        |   3 +-
 .../collections/permissions.cy.spec.js        |  10 +-
 .../personal-collections.cy.spec.js           |  31 ++-
 .../scenarios/models/models.cy.spec.js        |  79 --------
 .../moderation-collection.cy.spec.js          |  11 +-
 .../timelines-collection.cy.spec.js           |   3 +-
 ...llogical-UI-elements-for-nodata.cy.spec.js |  10 +-
 34 files changed, 525 insertions(+), 522 deletions(-)
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionBookmark.tsx
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.styled.tsx
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.tsx
 delete mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx
 delete mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.tsx
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.tsx
 delete mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.unit.spec.js
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionMenu.tsx
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/CollectionTimeline.tsx
 create mode 100644 frontend/src/metabase/collections/components/CollectionHeader/index.ts
 create mode 100644 frontend/src/metabase/collections/components/MoveCollectionModal/MoveCollectionModal.tsx
 create mode 100644 frontend/src/metabase/collections/components/MoveCollectionModal/index.ts
 create mode 100644 frontend/src/metabase/collections/containers/MoveCollectionModal/MoveCollectionModal.tsx
 create mode 100644 frontend/src/metabase/collections/containers/MoveCollectionModal/index.ts
 create mode 100644 frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx
 create mode 100644 frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.styled.tsx
 create mode 100644 frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.tsx
 create mode 100644 frontend/src/metabase/core/components/BookmarkToggle/index.ts

diff --git a/frontend/src/metabase-types/api/collection.ts b/frontend/src/metabase-types/api/collection.ts
index 72f25665aee..f8dffc2d082 100644
--- a/frontend/src/metabase-types/api/collection.ts
+++ b/frontend/src/metabase-types/api/collection.ts
@@ -7,6 +7,7 @@ export type CollectionAuthorityLevel = "official" | null;
 export interface Collection {
   id: CollectionId;
   name: string;
+  description: string | null;
   can_write: boolean;
   archived: boolean;
   children?: Collection[];
diff --git a/frontend/src/metabase-types/api/mocks/collection.ts b/frontend/src/metabase-types/api/mocks/collection.ts
index 19d524d04bc..15ede2ae3c1 100644
--- a/frontend/src/metabase-types/api/mocks/collection.ts
+++ b/frontend/src/metabase-types/api/mocks/collection.ts
@@ -5,6 +5,7 @@ export const createMockCollection = (
 ): Collection => ({
   id: 1,
   name: "Collection",
+  description: null,
   can_write: false,
   archived: false,
   ...opts,
diff --git a/frontend/src/metabase/collections/components/CollectionEmptyState/CollectionEmptyState.tsx b/frontend/src/metabase/collections/components/CollectionEmptyState/CollectionEmptyState.tsx
index 22f45376e4a..7ba3d7f294a 100644
--- a/frontend/src/metabase/collections/components/CollectionEmptyState/CollectionEmptyState.tsx
+++ b/frontend/src/metabase/collections/components/CollectionEmptyState/CollectionEmptyState.tsx
@@ -3,6 +3,7 @@ import { t } from "ttag";
 import Button from "metabase/core/components/Button";
 import NewItemMenu from "metabase/containers/NewItemMenu";
 import { ANALYTICS_CONTEXT } from "metabase/collections/constants";
+import { CollectionId } from "metabase-types/api";
 import {
   EmptyStateDescription,
   EmptyStateIconBackground,
@@ -11,7 +12,13 @@ import {
   EmptyStateTitle,
 } from "./CollectionEmptyState.styled";
 
-const CollectionEmptyState = (): JSX.Element => {
+export interface CollectionEmptyStateProps {
+  collectionId?: CollectionId;
+}
+
+const CollectionEmptyState = ({
+  collectionId,
+}: CollectionEmptyStateProps): JSX.Element => {
   return (
     <EmptyStateRoot data-testid="collection-empty-state">
       <CollectionEmptyIcon />
@@ -19,6 +26,7 @@ const CollectionEmptyState = (): JSX.Element => {
       <EmptyStateDescription>{t`Use collections to organize and group dashboards and questions for your team or yourself`}</EmptyStateDescription>
       <NewItemMenu
         trigger={<Button icon="add">{t`Create a new…`}</Button>}
+        collectionId={collectionId}
         analyticsContext={ANALYTICS_CONTEXT}
       />
     </EmptyStateRoot>
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionBookmark.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionBookmark.tsx
new file mode 100644
index 00000000000..c2cd55a75ea
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionBookmark.tsx
@@ -0,0 +1,42 @@
+import React, { useCallback } from "react";
+import BookmarkToggle from "metabase/core/components/BookmarkToggle";
+import { isRootCollection } from "metabase/collections/utils";
+import { Collection } from "metabase-types/api";
+
+export interface CollectionBookmarkProps {
+  collection: Collection;
+  isBookmarked: boolean;
+  onCreateBookmark: (collection: Collection) => void;
+  onDeleteBookmark: (collection: Collection) => void;
+}
+
+const CollectionBookmark = ({
+  collection,
+  isBookmarked,
+  onCreateBookmark,
+  onDeleteBookmark,
+}: CollectionBookmarkProps): JSX.Element | null => {
+  const isRoot = isRootCollection(collection);
+
+  const handleCreateBookmark = useCallback(() => {
+    onCreateBookmark(collection);
+  }, [collection, onCreateBookmark]);
+
+  const handleDeleteBookmark = useCallback(() => {
+    onDeleteBookmark(collection);
+  }, [collection, onDeleteBookmark]);
+
+  if (isRoot) {
+    return null;
+  }
+
+  return (
+    <BookmarkToggle
+      isBookmarked={isBookmarked}
+      onCreateBookmark={handleCreateBookmark}
+      onDeleteBookmark={handleDeleteBookmark}
+    />
+  );
+};
+
+export default CollectionBookmark;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.styled.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.styled.tsx
new file mode 100644
index 00000000000..c8a102fc7fd
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.styled.tsx
@@ -0,0 +1,21 @@
+import styled from "@emotion/styled";
+
+export const CaptionContainer = styled.div`
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+`;
+
+export const CaptionTitle = styled.h1`
+  font-weight: 900;
+  word-break: break-word;
+  word-wrap: anywhere;
+  overflow-wrap: anywhere;
+`;
+
+export const CaptionDescription = styled.div`
+  font-size: 1rem;
+  line-height: 1.5rem;
+  padding-top: 1.15rem;
+  max-width: 25rem;
+`;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.tsx
new file mode 100644
index 00000000000..747b9bbc3b0
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionCaption.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { PLUGIN_COLLECTION_COMPONENTS } from "metabase/plugins";
+import { Collection } from "metabase-types/api";
+import {
+  CaptionContainer,
+  CaptionTitle,
+  CaptionDescription,
+} from "./CollectionCaption.styled";
+
+export interface CollectionCaptionProps {
+  collection: Collection;
+}
+
+const CollectionCaption = ({
+  collection,
+}: CollectionCaptionProps): JSX.Element => {
+  return (
+    <div>
+      <CaptionContainer>
+        <PLUGIN_COLLECTION_COMPONENTS.CollectionAuthorityLevelIcon
+          collection={collection}
+          size={24}
+        />
+        <CaptionTitle data-testid="collection-name-heading">
+          {collection.name}
+        </CaptionTitle>
+      </CaptionContainer>
+      {collection.description && (
+        <CaptionDescription>{collection.description}</CaptionDescription>
+      )}
+    </div>
+  );
+};
+
+export default CollectionCaption;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx
deleted file mode 100644
index 10710fe8c4c..00000000000
--- a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.jsx
+++ /dev/null
@@ -1,176 +0,0 @@
-/* eslint-disable react/prop-types */
-import React, { useState } from "react";
-import { t } from "ttag";
-
-import * as Urls from "metabase/lib/urls";
-import { isPersonalCollection } from "metabase/collections/utils";
-import Icon, { IconWrapper } from "metabase/components/Icon";
-import Link from "metabase/core/components/Link";
-import PageHeading from "metabase/components/type/PageHeading";
-import Tooltip from "metabase/components/Tooltip";
-
-import CollectionEditMenu from "metabase/collections/components/CollectionEditMenu";
-import NewItemMenu from "metabase/containers/NewItemMenu";
-import { color } from "metabase/lib/colors";
-
-import { PLUGIN_COLLECTION_COMPONENTS } from "metabase/plugins";
-import { ANALYTICS_CONTEXT } from "metabase/collections/constants";
-
-import {
-  BookmarkIcon,
-  BookmarkIconWrapper,
-  Container,
-  DescriptionHeading,
-  MenuContainer,
-  TitleContent,
-} from "./CollectionHeader.styled";
-
-function Title({ collection }) {
-  return (
-    <div>
-      <TitleContent>
-        <PLUGIN_COLLECTION_COMPONENTS.CollectionAuthorityLevelIcon
-          collection={collection}
-          mr={1}
-          size={24}
-        />
-        <PageHeading
-          data-testid="collection-name-heading"
-          className="text-wrap"
-        >
-          {collection.name}
-        </PageHeading>
-      </TitleContent>
-      {collection.description && (
-        <DescriptionHeading>{collection.description}</DescriptionHeading>
-      )}
-    </div>
-  );
-}
-
-function PermissionsLink({
-  collection,
-  isAdmin,
-  isPersonal,
-  isPersonalCollectionChild,
-}) {
-  const tooltip = t`Edit the permissions for this collection`;
-  const link = `${Urls.collection(collection)}/permissions`;
-
-  const canChangePermissions =
-    isAdmin && !isPersonal && !isPersonalCollectionChild;
-
-  return canChangePermissions ? (
-    <Tooltip tooltip={tooltip}>
-      <Link to={link}>
-        <IconWrapper>
-          <Icon name="lock" />
-        </IconWrapper>
-      </Link>
-    </Tooltip>
-  ) : null;
-}
-
-function TimelinesLink({ collection }) {
-  const title = t`Events`;
-  const link = Urls.timelinesInCollection(collection);
-
-  return (
-    <Tooltip tooltip={title}>
-      <Link to={link}>
-        <IconWrapper>
-          <Icon name="calendar" size={20} />
-        </IconWrapper>
-      </Link>
-    </Tooltip>
-  );
-}
-
-function EditMenu({
-  collection,
-  hasWritePermission,
-  isAdmin,
-  isPersonal,
-  isRoot,
-}) {
-  const tooltip = t`Edit collection`;
-
-  const canEditCollection = hasWritePermission && !isPersonal;
-
-  return canEditCollection ? (
-    <CollectionEditMenu
-      tooltip={tooltip}
-      collection={collection}
-      isAdmin={isAdmin}
-      isRoot={isRoot}
-    />
-  ) : null;
-}
-
-function Bookmark({ isBookmarked, onClickBookmark }) {
-  const title = isBookmarked ? t`Remove from bookmarks` : t`Bookmark`;
-  const iconColor = isBookmarked ? color("brand") : "";
-  const [animation, setAnimation] = useState(null);
-
-  const handleClickBookmark = () => {
-    onClickBookmark();
-    setAnimation(isBookmarked ? "shrink" : "expand");
-  };
-
-  return (
-    <Tooltip tooltip={title}>
-      <BookmarkIconWrapper
-        isBookmarked={isBookmarked}
-        onClick={handleClickBookmark}
-      >
-        <BookmarkIcon
-          name="bookmark"
-          color={iconColor}
-          size={20}
-          animation={animation}
-        />
-      </BookmarkIconWrapper>
-    </Tooltip>
-  );
-}
-
-function Menu(props) {
-  const { collectionId, hasWritePermission } = props;
-
-  const shouldBeBookmarkable = collectionId !== "root";
-
-  return (
-    <MenuContainer data-testid="collection-menu">
-      {hasWritePermission && (
-        <NewItemMenu
-          {...props}
-          collectionId={collectionId}
-          triggerIcon="add"
-          triggerTooltip={t`New…`}
-          analyticsContext={ANALYTICS_CONTEXT}
-        />
-      )}
-      <EditMenu {...props} />
-      <PermissionsLink {...props} />
-      <TimelinesLink {...props} />
-      {shouldBeBookmarkable && <Bookmark {...props} />}
-    </MenuContainer>
-  );
-}
-
-export default function CollectionHeader(props) {
-  const { collection } = props;
-  const isPersonal = isPersonalCollection(collection);
-  const hasWritePermission = collection && collection.can_write;
-
-  return (
-    <Container>
-      <Title {...props} />
-      <Menu
-        {...props}
-        isPersonal={isPersonal}
-        hasWritePermission={hasWritePermission}
-      />
-    </Container>
-  );
-}
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx
deleted file mode 100644
index caa1dbea936..00000000000
--- a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.jsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import styled from "@emotion/styled";
-import { css } from "@emotion/react";
-
-import { color } from "metabase/lib/colors";
-import { breakpointMinSmall, space } from "metabase/styled-components/theme";
-import {
-  shrinkOrExpandOnClick,
-  shrinkOrExpandDuration,
-} from "metabase/styled-components/theme/button.ts";
-
-import Icon, { IconWrapper } from "metabase/components/Icon";
-
-export const BookmarkIconWrapper = styled(IconWrapper)`
-  ${props =>
-    !props.isBookmarked &&
-    css`
-      &:hover {
-        ${BookmarkIcon} {
-          color: ${color("text-dark")};
-        }
-      }
-    `}
-`;
-export const BookmarkIcon = styled(Icon)`
-  ${shrinkOrExpandOnClick}
-
-  ${props =>
-    props.animation === "expand" &&
-    css`
-      animation: expand linear ${shrinkOrExpandDuration};
-    `}
-
-  ${props =>
-    props.animation === "shrink" &&
-    css`
-      animation: shrink linear ${shrinkOrExpandDuration};
-    `}
-`;
-
-export const Container = styled.div`
-  display: flex;
-  justify-content: space-between;
-  flex-direction: column;
-  margin-bottom: ${space(3)};
-  padding-top: ${space(0)};
-
-  ${breakpointMinSmall} {
-    align-items: center;
-    flex-direction: row;
-    padding-top: ${space(1)};
-  }
-`;
-
-export const MenuContainer = styled.div`
-  display: flex;
-  margin-top: ${space(1)};
-  align-self: start;
-`;
-
-export const DescriptionTooltipIcon = styled(Icon)`
-  color: ${color("bg-dark")};
-  margin-left: ${space(1)};
-  margin-right: ${space(1)};
-  margin-top: ${space(0)};
-
-  &:hover {
-    color: ${color("brand")};
-  }
-`;
-
-DescriptionTooltipIcon.defaultProps = {
-  name: "info",
-};
-
-export const DescriptionHeading = styled.div`
-  font-size: 1rem;
-  line-height: 1.5rem;
-  padding-top: 1.15rem;
-  max-width: 400px;
-`;
-
-export const TitleContent = styled.div`
-  display: flex;
-  align-items: center;
-`;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.tsx
new file mode 100644
index 00000000000..f76993a1f78
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.styled.tsx
@@ -0,0 +1,22 @@
+import styled from "@emotion/styled";
+import { breakpointMinSmall } from "metabase/styled-components/theme";
+
+export const HeaderRoot = styled.div`
+  display: flex;
+  justify-content: space-between;
+  flex-direction: column;
+  margin-bottom: 2rem;
+  padding-top: 0.25rem;
+
+  ${breakpointMinSmall} {
+    align-items: center;
+    flex-direction: row;
+    padding-top: 0.5rem;
+  }
+`;
+
+export const HeaderActions = styled.div`
+  display: flex;
+  margin-top: 0.5rem;
+  align-self: start;
+`;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.tsx
new file mode 100644
index 00000000000..c0752a4c54b
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.tsx
@@ -0,0 +1,47 @@
+import React from "react";
+import { Collection } from "metabase-types/api";
+import CollectionCaption from "./CollectionCaption";
+import CollectionBookmark from "./CollectionBookmark";
+import CollectionMenu from "./CollectionMenu";
+import CollectionTimeline from "./CollectionTimeline";
+import { HeaderActions, HeaderRoot } from "./CollectionHeader.styled";
+
+export interface CollectionHeaderProps {
+  collection: Collection;
+  isAdmin: boolean;
+  isBookmarked: boolean;
+  isPersonalCollectionChild: boolean;
+  onCreateBookmark: (collection: Collection) => void;
+  onDeleteBookmark: (collection: Collection) => void;
+}
+
+const CollectionHeader = ({
+  collection,
+  isAdmin,
+  isBookmarked,
+  isPersonalCollectionChild,
+  onCreateBookmark,
+  onDeleteBookmark,
+}: CollectionHeaderProps): JSX.Element => {
+  return (
+    <HeaderRoot>
+      <CollectionCaption collection={collection} />
+      <HeaderActions data-testid="collection-menu">
+        <CollectionTimeline collection={collection} />
+        <CollectionBookmark
+          collection={collection}
+          isBookmarked={isBookmarked}
+          onCreateBookmark={onCreateBookmark}
+          onDeleteBookmark={onDeleteBookmark}
+        />
+        <CollectionMenu
+          collection={collection}
+          isAdmin={isAdmin}
+          isPersonalCollectionChild={isPersonalCollectionChild}
+        />
+      </HeaderActions>
+    </HeaderRoot>
+  );
+};
+
+export default CollectionHeader;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.unit.spec.js b/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.unit.spec.js
deleted file mode 100644
index 80ea2e0da46..00000000000
--- a/frontend/src/metabase/collections/components/CollectionHeader/CollectionHeader.unit.spec.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import React from "react";
-import { screen } from "@testing-library/react";
-import { renderWithProviders } from "__support__/ui";
-import Header from "./CollectionHeader";
-
-const collection = {
-  name: "Name",
-};
-
-it("should display collection name", () => {
-  renderWithProviders(<Header collection={collection} />);
-
-  screen.getByText(collection.name);
-});
-
-describe("description tooltip", () => {
-  describe("should not be displayed", () => {
-    it("if description is not received", () => {
-      const { container } = renderWithProviders(
-        <Header collection={collection} />,
-      );
-      expect(container.textContent).toEqual("Name");
-    });
-  });
-
-  describe("should be displayed", () => {
-    it("if description is received", () => {
-      const description = "description";
-
-      renderWithProviders(
-        <Header collection={{ ...collection, description }} />,
-      );
-
-      screen.getByText(description);
-    });
-  });
-});
-
-describe("permissions link", () => {
-  const ariaLabel = "lock icon";
-
-  describe("should not be displayed", () => {
-    it("if user is not admin", () => {
-      renderWithProviders(<Header collection={collection} />);
-
-      expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
-    });
-
-    it("for personal collections", () => {
-      renderWithProviders(
-        <Header
-          isAdmin={true}
-          collection={{ ...collection, personal_owner_id: 1 }}
-        />,
-      );
-
-      expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
-    });
-
-    it("if a collection is a personal collection child", () => {
-      renderWithProviders(
-        <Header
-          isAdmin={true}
-          collection={collection}
-          isPersonalCollectionChild={true}
-        />,
-      );
-
-      expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
-    });
-  });
-
-  describe("should be displayed", () => {
-    it("if user is admin", () => {
-      renderWithProviders(<Header collection={collection} isAdmin={true} />);
-
-      screen.getByLabelText(ariaLabel);
-    });
-  });
-});
-
-describe("link to add new collection items", () => {
-  const ariaLabel = "add icon";
-
-  describe("should not be displayed", () => {
-    it("when no detail is passed in the collection to determine if user can change collection", () => {
-      renderWithProviders(<Header collection={collection} />);
-
-      expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
-    });
-
-    it("if user is not allowed to change collection", () => {
-      renderWithProviders(
-        <Header collection={{ ...collection, can_write: false }} />,
-      );
-
-      expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
-    });
-  });
-
-  describe("should be displayed", () => {
-    it("if user is allowed to change collection", () => {
-      renderWithProviders(
-        <Header collection={{ ...collection, can_write: true }} />,
-      );
-
-      screen.getByLabelText(ariaLabel);
-    });
-  });
-});
-
-describe("link to add new collection items", () => {
-  const ariaLabel = "add icon";
-
-  describe("should not be displayed", () => {
-    it("if user is not allowed to change collection", () => {
-      renderWithProviders(<Header collection={collection} />);
-
-      expect(screen.queryByLabelText(ariaLabel)).not.toBeInTheDocument();
-    });
-  });
-
-  describe("should be displayed", () => {
-    it("if user is allowed to change collection", () => {
-      renderWithProviders(
-        <Header collection={{ ...collection, can_write: true }} />,
-      );
-
-      screen.getByLabelText(ariaLabel);
-    });
-  });
-});
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionMenu.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionMenu.tsx
new file mode 100644
index 00000000000..c1f826a957c
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionMenu.tsx
@@ -0,0 +1,68 @@
+import React from "react";
+import { t } from "ttag";
+import * as Urls from "metabase/lib/urls";
+import EntityMenu from "metabase/components/EntityMenu";
+import { ANALYTICS_CONTEXT } from "metabase/collections/constants";
+import {
+  isPersonalCollection,
+  isRootCollection,
+} from "metabase/collections/utils";
+import { Collection } from "metabase-types/api";
+
+export interface CollectionMenuProps {
+  collection: Collection;
+  isAdmin: boolean;
+  isPersonalCollectionChild: boolean;
+}
+
+const CollectionMenu = ({
+  collection,
+  isAdmin,
+  isPersonalCollectionChild,
+}: CollectionMenuProps): JSX.Element | null => {
+  const items = [];
+  const url = Urls.collection(collection);
+  const isRoot = isRootCollection(collection);
+  const isPersonal = isPersonalCollection(collection);
+  const canWrite = collection.can_write;
+
+  if (!isRoot && !isPersonal && canWrite) {
+    items.push(
+      {
+        title: t`Edit this collection`,
+        icon: "edit_document",
+        link: `${url}/edit`,
+        event: `${ANALYTICS_CONTEXT};Edit Menu;Edit Collection Click`,
+      },
+      {
+        title: t`Move`,
+        icon: "move",
+        link: `${url}/move`,
+        event: `${ANALYTICS_CONTEXT};Edit Menu;Move Collection`,
+      },
+      {
+        title: t`Archive`,
+        icon: "view_archive",
+        link: `${url}/archive`,
+        event: `${ANALYTICS_CONTEXT};Edit Menu;Archive Collection`,
+      },
+    );
+  }
+
+  if (isAdmin && !isPersonal && !isPersonalCollectionChild) {
+    items.push({
+      title: t`Edit permissions`,
+      icon: "lock",
+      link: `${url}/permissions`,
+      event: `${ANALYTICS_CONTEXT};Edit Menu;Edit Permissions`,
+    });
+  }
+
+  if (items.length > 0) {
+    return <EntityMenu items={items} triggerIcon="ellipsis" />;
+  } else {
+    return null;
+  }
+};
+
+export default CollectionMenu;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/CollectionTimeline.tsx b/frontend/src/metabase/collections/components/CollectionHeader/CollectionTimeline.tsx
new file mode 100644
index 00000000000..1408c4362d5
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/CollectionTimeline.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { t } from "ttag";
+import * as Urls from "metabase/lib/urls";
+import Icon, { IconWrapper } from "metabase/components/Icon";
+import Link from "metabase/core/components/Link/Link";
+import Tooltip from "metabase/components/Tooltip";
+import { Collection } from "metabase-types/api";
+
+interface CollectionTimelineProps {
+  collection: Collection;
+}
+
+const CollectionTimeline = ({
+  collection,
+}: CollectionTimelineProps): JSX.Element => {
+  const url = Urls.timelinesInCollection(collection);
+
+  return (
+    <Tooltip tooltip={t`Events`}>
+      <Link to={url}>
+        <IconWrapper>
+          <Icon name="calendar" size={20} />
+        </IconWrapper>
+      </Link>
+    </Tooltip>
+  );
+};
+
+export default CollectionTimeline;
diff --git a/frontend/src/metabase/collections/components/CollectionHeader/index.ts b/frontend/src/metabase/collections/components/CollectionHeader/index.ts
new file mode 100644
index 00000000000..6dd850ae196
--- /dev/null
+++ b/frontend/src/metabase/collections/components/CollectionHeader/index.ts
@@ -0,0 +1 @@
+export { default } from "./CollectionHeader";
diff --git a/frontend/src/metabase/collections/components/MoveCollectionModal/MoveCollectionModal.tsx b/frontend/src/metabase/collections/components/MoveCollectionModal/MoveCollectionModal.tsx
new file mode 100644
index 00000000000..b9f8ce2e71f
--- /dev/null
+++ b/frontend/src/metabase/collections/components/MoveCollectionModal/MoveCollectionModal.tsx
@@ -0,0 +1,34 @@
+import React, { useCallback } from "react";
+import CollectionMoveModal from "metabase/containers/CollectionMoveModal";
+import { Collection } from "metabase-types/api";
+
+export interface MoveCollectionModalProps {
+  collection: Collection;
+  onMove: (source: Collection, destination: Collection) => void;
+  onClose: () => void;
+}
+
+const MoveCollectionModal = ({
+  collection,
+  onMove,
+  onClose,
+}: MoveCollectionModalProps): JSX.Element => {
+  const handleMove = useCallback(
+    async (destination: Collection) => {
+      await onMove(collection, destination);
+      onClose();
+    },
+    [collection, onMove, onClose],
+  );
+
+  return (
+    <CollectionMoveModal
+      title={`Move ${collection.name}?`}
+      initialCollectionId={collection.id}
+      onMove={handleMove}
+      onClose={onClose}
+    />
+  );
+};
+
+export default MoveCollectionModal;
diff --git a/frontend/src/metabase/collections/components/MoveCollectionModal/index.ts b/frontend/src/metabase/collections/components/MoveCollectionModal/index.ts
new file mode 100644
index 00000000000..787d35ca27f
--- /dev/null
+++ b/frontend/src/metabase/collections/components/MoveCollectionModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./MoveCollectionModal";
diff --git a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx
index eb8ce7bc24f..614432bae20 100644
--- a/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx
+++ b/frontend/src/metabase/collections/components/PinnedItemCard/PinnedItemCard.stories.tsx
@@ -13,6 +13,7 @@ const collection = {
   can_write: true,
   id: 1,
   name: "Collection Foo",
+  description: null,
   archived: false,
 };
 
diff --git a/frontend/src/metabase/collections/containers/CollectionContent.jsx b/frontend/src/metabase/collections/containers/CollectionContent.jsx
index e8bdef14651..3dfdd528419 100644
--- a/frontend/src/metabase/collections/containers/CollectionContent.jsx
+++ b/frontend/src/metabase/collections/containers/CollectionContent.jsx
@@ -63,7 +63,6 @@ function CollectionContent({
   createBookmark,
   deleteBookmark,
   isAdmin,
-  isRoot,
   metadata,
   isNavbarOpen,
   openNavbar,
@@ -156,9 +155,12 @@ function CollectionContent({
     setSelectedAction("copy");
   };
 
-  const handleClickBookmark = () => {
-    const toggleBookmark = isBookmarked ? deleteBookmark : createBookmark;
-    toggleBookmark(collectionId, "collection");
+  const handleCreateBookmark = () => {
+    createBookmark(collectionId, "collection");
+  };
+
+  const handleDeleteBookmark = () => {
+    deleteBookmark(collectionId, "collection");
   };
 
   const unpinnedQuery = {
@@ -191,16 +193,15 @@ function CollectionContent({
           <CollectionRoot>
             <CollectionMain>
               <Header
-                onClickBookmark={handleClickBookmark}
-                isBookmarked={isBookmarked}
-                isRoot={isRoot}
-                isAdmin={isAdmin}
-                collectionId={collectionId}
                 collection={collection}
+                isAdmin={isAdmin}
+                isBookmarked={isBookmarked}
                 isPersonalCollectionChild={isPersonalCollectionChild(
                   collection,
                   collectionList,
                 )}
+                onCreateBookmark={handleCreateBookmark}
+                onDeleteBookmark={handleDeleteBookmark}
               />
               <PinnedItemOverview
                 bookmarks={bookmarks}
@@ -242,7 +243,7 @@ function CollectionContent({
                   if (isEmpty && !loadingUnpinnedItems) {
                     return (
                       <CollectionEmptyContent>
-                        <CollectionEmptyState />
+                        <CollectionEmptyState collectionId={collectionId} />
                       </CollectionEmptyContent>
                     );
                   }
diff --git a/frontend/src/metabase/collections/containers/MoveCollectionModal/MoveCollectionModal.tsx b/frontend/src/metabase/collections/containers/MoveCollectionModal/MoveCollectionModal.tsx
new file mode 100644
index 00000000000..27c36ede31a
--- /dev/null
+++ b/frontend/src/metabase/collections/containers/MoveCollectionModal/MoveCollectionModal.tsx
@@ -0,0 +1,28 @@
+import { connect } from "react-redux";
+import _ from "underscore";
+import * as Urls from "metabase/lib/urls";
+import Collections from "metabase/entities/collections";
+import { State } from "metabase-types/store";
+import MoveCollectionModal from "../../components/MoveCollectionModal";
+
+interface MoveCollectionModalProps {
+  params: MoveCollectionModalParams;
+}
+
+interface MoveCollectionModalParams {
+  slug: string;
+}
+
+const collectionProps = {
+  id: (state: State, props: MoveCollectionModalProps) =>
+    Urls.extractCollectionId(props.params.slug),
+};
+
+const mapDispatchToProps = {
+  onMove: Collections.actions.setCollection,
+};
+
+export default _.compose(
+  Collections.load(collectionProps),
+  connect(null, mapDispatchToProps),
+)(MoveCollectionModal);
diff --git a/frontend/src/metabase/collections/containers/MoveCollectionModal/index.ts b/frontend/src/metabase/collections/containers/MoveCollectionModal/index.ts
new file mode 100644
index 00000000000..787d35ca27f
--- /dev/null
+++ b/frontend/src/metabase/collections/containers/MoveCollectionModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./MoveCollectionModal";
diff --git a/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx
new file mode 100644
index 00000000000..946681031aa
--- /dev/null
+++ b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.stories.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import { ComponentStory } from "@storybook/react";
+import { useArgs } from "@storybook/client-api";
+import BookmarkToggle from "./BookmarkToggle";
+
+export default {
+  title: "Core/BookmarkToggle",
+  component: BookmarkToggle,
+};
+
+const Template: ComponentStory<typeof BookmarkToggle> = args => {
+  const [{ isBookmarked }, updateArgs] = useArgs();
+  const handleCreateBookmark = () => updateArgs({ isBookmarked: true });
+  const handleDeleteBookmark = () => updateArgs({ isBookmarked: false });
+
+  return (
+    <BookmarkToggle
+      {...args}
+      isBookmarked={isBookmarked}
+      onCreateBookmark={handleCreateBookmark}
+      onDeleteBookmark={handleDeleteBookmark}
+    />
+  );
+};
+
+export const Default = Template.bind({});
+Default.args = {
+  isBookmarked: false,
+};
diff --git a/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.styled.tsx b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.styled.tsx
new file mode 100644
index 00000000000..bea3650bde4
--- /dev/null
+++ b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.styled.tsx
@@ -0,0 +1,31 @@
+import styled from "@emotion/styled";
+import { keyframes } from "@emotion/react";
+import { color } from "metabase/lib/colors";
+import Icon from "metabase/components/Icon";
+
+const expandKeyframes = keyframes`
+  50% {
+    transform: scale(1.3);
+  }
+`;
+
+const shrinkKeyframes = keyframes`
+  50% {
+    transform: scale(0.8);
+  }
+`;
+
+export interface BookmarkIconProps {
+  isBookmarked: boolean;
+  isAnimating: boolean;
+  onAnimationEnd: () => void;
+}
+
+export const BookmarkIcon = styled(Icon)<BookmarkIconProps>`
+  color: ${props => (props.isBookmarked ? color("brand") : "")};
+  animation-name: ${props =>
+    props.isBookmarked ? expandKeyframes : shrinkKeyframes};
+  animation-play-state: ${props => (props.isAnimating ? "running" : "paused")};
+  animation-duration: 0.3s;
+  animation-timing-function: linear;
+`;
diff --git a/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.tsx b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.tsx
new file mode 100644
index 00000000000..7e809952117
--- /dev/null
+++ b/frontend/src/metabase/core/components/BookmarkToggle/BookmarkToggle.tsx
@@ -0,0 +1,65 @@
+import React, {
+  forwardRef,
+  HTMLAttributes,
+  Ref,
+  useCallback,
+  useState,
+} from "react";
+import { t } from "ttag";
+import { color } from "metabase/lib/colors";
+import { IconWrapper } from "metabase/components/Icon";
+import Tooltip from "metabase/components/Tooltip";
+import { BookmarkIcon } from "./BookmarkToggle.styled";
+
+export interface BookmarkToggleProps extends HTMLAttributes<HTMLDivElement> {
+  isBookmarked: boolean;
+  onCreateBookmark: () => void;
+  onDeleteBookmark: () => void;
+}
+
+const BookmarkToggle = forwardRef(function BookmarkToggle(
+  {
+    isBookmarked,
+    onCreateBookmark,
+    onDeleteBookmark,
+    ...props
+  }: BookmarkToggleProps,
+  ref: Ref<HTMLDivElement>,
+) {
+  const [isAnimating, setIsAnimating] = useState(false);
+
+  const handleClick = useCallback(() => {
+    if (isBookmarked) {
+      onDeleteBookmark();
+    } else {
+      onCreateBookmark();
+    }
+
+    setIsAnimating(true);
+  }, [isBookmarked, onCreateBookmark, onDeleteBookmark]);
+
+  const handleAnimationEnd = useCallback(() => {
+    setIsAnimating(false);
+  }, []);
+
+  return (
+    <Tooltip tooltip={isBookmarked ? t`Remove from bookmarks` : t`Bookmark`}>
+      <IconWrapper
+        {...props}
+        ref={ref}
+        hover={{ color: isBookmarked ? color("brand") : color("text-dark") }}
+        onClick={handleClick}
+      >
+        <BookmarkIcon
+          name="bookmark"
+          size={20}
+          isBookmarked={isBookmarked}
+          isAnimating={isAnimating}
+          onAnimationEnd={handleAnimationEnd}
+        />
+      </IconWrapper>
+    </Tooltip>
+  );
+});
+
+export default BookmarkToggle;
diff --git a/frontend/src/metabase/core/components/BookmarkToggle/index.ts b/frontend/src/metabase/core/components/BookmarkToggle/index.ts
new file mode 100644
index 00000000000..b1dae5dacf4
--- /dev/null
+++ b/frontend/src/metabase/core/components/BookmarkToggle/index.ts
@@ -0,0 +1 @@
+export { default } from "./BookmarkToggle";
diff --git a/frontend/src/metabase/nav/containers/AppBar.tsx b/frontend/src/metabase/nav/containers/AppBar.tsx
index 252c3f77097..2f7e46b7e2e 100644
--- a/frontend/src/metabase/nav/containers/AppBar.tsx
+++ b/frontend/src/metabase/nav/containers/AppBar.tsx
@@ -6,12 +6,12 @@ import { withRouter } from "react-router";
 
 import Tooltip from "metabase/components/Tooltip";
 import LogoIcon from "metabase/components/LogoIcon";
-
 import SearchBar from "metabase/nav/components/SearchBar";
 import SidebarButton from "metabase/nav/components/SidebarButton";
 import NewItemButton from "metabase/nav/components/NewItemButton";
 import PathBreadcrumbs from "../components/PathBreadcrumbs/PathBreadcrumbs";
 
+import { CollectionId } from "metabase-types/api";
 import { State } from "metabase-types/store";
 
 import { getIsNavbarOpen, closeNavbar, toggleNavbar } from "metabase/redux/app";
@@ -41,7 +41,7 @@ type Props = {
   isNavBarVisible: boolean;
   isSearchVisible: boolean;
   isNewButtonVisible: boolean;
-  collectionId: string;
+  collectionId?: CollectionId;
   showBreadcrumb: boolean;
   toggleNavbar: () => void;
   closeNavbar: () => void;
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index 8c69e758bbe..a269409668d 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -35,6 +35,7 @@ import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 
 import CollectionEdit from "metabase/collections/containers/CollectionEdit";
 import CollectionCreate from "metabase/collections/containers/CollectionCreate";
+import MoveCollectionModal from "metabase/collections/containers/MoveCollectionModal";
 import ArchiveCollectionModal from "metabase/components/ArchiveCollectionModal";
 import CollectionPermissionsModal from "metabase/admin/permissions/components/CollectionPermissionsModal/CollectionPermissionsModal";
 import UserCollectionList from "metabase/containers/UserCollectionList";
@@ -218,6 +219,7 @@ export const getRoutes = store => (
 
         <Route path="collection/:slug" component={CollectionLanding}>
           <ModalRoute path="edit" modal={CollectionEdit} />
+          <ModalRoute path="move" modal={MoveCollectionModal} />
           <ModalRoute path="archive" modal={ArchiveCollectionModal} />
           <ModalRoute path="new_collection" modal={CollectionCreate} />
           <ModalRoute path="new_dashboard" modal={CreateDashboardModal} />
diff --git a/frontend/test/__support__/e2e/helpers/e2e-collection-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-collection-helpers.js
index 22b66b8d080..0ca80a18153 100644
--- a/frontend/test/__support__/e2e/helpers/e2e-collection-helpers.js
+++ b/frontend/test/__support__/e2e/helpers/e2e-collection-helpers.js
@@ -5,14 +5,20 @@ import { popover } from "__support__/e2e/cypress";
  * @param {"question" | "dashboard" | "collection"} type
  */
 export function openNewCollectionItemFlowFor(type) {
-  cy.findByTestId("collection-menu").within(() => {
-    cy.icon("add").click();
-  });
+  cy.findByText("New").click();
   popover()
     .findByText(new RegExp(type, "i"))
     .click();
 }
 
+export function getCollectionActions() {
+  return cy.findByTestId("collection-menu");
+}
+
+export function openCollectionMenu() {
+  getCollectionActions().within(() => cy.icon("ellipsis").click());
+}
+
 export function getSidebarSectionTitle(name) {
   return cy.findAllByRole("heading", { name });
 }
diff --git a/frontend/test/metabase/scenarios/collections/collections.cy.spec.js b/frontend/test/metabase/scenarios/collections/collections.cy.spec.js
index 6e53b18fa8e..445bb6593ca 100644
--- a/frontend/test/metabase/scenarios/collections/collections.cy.spec.js
+++ b/frontend/test/metabase/scenarios/collections/collections.cy.spec.js
@@ -7,6 +7,7 @@ import {
   getCollectionIdFromSlug,
   openNavigationSidebar,
   closeNavigationSidebar,
+  openCollectionMenu,
 } from "__support__/e2e/cypress";
 import { displaySidebarChildOf } from "./helpers/e2e-collections-sidebar.js";
 import { USERS, USER_GROUPS } from "__support__/e2e/cypress_data";
@@ -471,7 +472,7 @@ function ensureCollectionIsExpanded(collection, { children = [] } = {}) {
 }
 
 function moveOpenedCollectionTo(newParent) {
-  cy.icon("pencil").click();
+  openCollectionMenu();
   cy.findByTextEnsureVisible("Edit this collection").click();
 
   // Open the select dropdown menu
diff --git a/frontend/test/metabase/scenarios/collections/permissions.cy.spec.js b/frontend/test/metabase/scenarios/collections/permissions.cy.spec.js
index 0ff9b24edf1..9257c2b545c 100644
--- a/frontend/test/metabase/scenarios/collections/permissions.cy.spec.js
+++ b/frontend/test/metabase/scenarios/collections/permissions.cy.spec.js
@@ -6,6 +6,7 @@ import {
   appBar,
   navigationSidebar,
   openNativeEditor,
+  openCollectionMenu,
 } from "__support__/e2e/cypress";
 
 import { displaySidebarChildOf } from "./helpers/e2e-collections-sidebar.js";
@@ -222,9 +223,8 @@ describe("collection permissions", () => {
                       cy.visit(`/collection/${THIRD_COLLECTION_ID}`);
                     });
 
-                    cy.icon("pencil").click();
-
-                    cy.findByText("Archive this collection").click();
+                    openCollectionMenu();
+                    popover().within(() => cy.findByText("Archive").click());
                     cy.get(".Modal")
                       .findByText("Archive")
                       .click();
@@ -314,8 +314,8 @@ describe("collection permissions", () => {
                         collection => collection.slug === "third_collection",
                       );
                       cy.visit(`/collection/${THIRD_COLLECTION_ID}`);
-                      cy.icon("pencil").click();
-                      cy.findByText("Archive this collection").click();
+                      openCollectionMenu();
+                      popover().within(() => cy.findByText("Archive").click());
                       cy.get(".Modal")
                         .findByText("Cancel")
                         .click();
diff --git a/frontend/test/metabase/scenarios/collections/personal-collections.cy.spec.js b/frontend/test/metabase/scenarios/collections/personal-collections.cy.spec.js
index 55b8e928e3a..5e2762facdf 100644
--- a/frontend/test/metabase/scenarios/collections/personal-collections.cy.spec.js
+++ b/frontend/test/metabase/scenarios/collections/personal-collections.cy.spec.js
@@ -4,6 +4,8 @@ import {
   modal,
   navigationSidebar,
   openNewCollectionItemFlowFor,
+  getCollectionActions,
+  openCollectionMenu,
 } from "__support__/e2e/cypress";
 
 import { USERS } from "__support__/e2e/cypress_data";
@@ -54,10 +56,8 @@ describe("personal collections", () => {
       cy.visit("/collection/root");
       cy.findByText("Your personal collection").click();
 
-      cy.findByTestId("collection-menu").within(() => {
-        cy.icon("add");
-        cy.icon("lock").should("not.exist");
-        cy.icon("pencil").should("not.exist");
+      getCollectionActions().within(() => {
+        cy.icon("ellipsis").should("not.exist");
       });
 
       // This leads to an infinite loop and a timeout in the CI
@@ -75,10 +75,11 @@ describe("personal collections", () => {
         .findByText("Foo")
         .click();
 
-      cy.findByTestId("collection-menu").within(() => {
-        // It should be possible to edit sub-collections' details, but not its permissions
-        cy.icon("pencil");
-        cy.icon("lock").should("not.exist");
+      // It should be possible to edit sub-collections' details, but not its permissions
+      openCollectionMenu();
+      popover().within(() => {
+        cy.findByText("Edit this collection").should("be.visible");
+        cy.findByText("Edit permissions").should("not.exist");
       });
 
       // Check that it's not possible to open permissions modal via URL for personal collection child
@@ -91,10 +92,8 @@ describe("personal collections", () => {
       // Go to random user's personal collection
       cy.visit("/collection/5");
 
-      cy.findByTestId("collection-menu").within(() => {
-        cy.icon("add");
-        cy.icon("lock").should("not.exist");
-        cy.icon("pencil").should("not.exist");
+      getCollectionActions().within(() => {
+        cy.icon("ellipsis").should("not.exist");
       });
     });
 
@@ -131,7 +130,7 @@ describe("personal collections", () => {
           cy.get("@sidebar")
             .findByText("Bar")
             .click();
-          cy.icon("pencil").click();
+          openCollectionMenu();
           /**
            * We're testing a few things here:
            *  1. editing collection's title
@@ -139,7 +138,7 @@ describe("personal collections", () => {
            *  3. moving that collection within personal collection
            *  4. archiving the collection within personal collection (metabase#15343)
            */
-          cy.findByText("Edit this collection").click();
+          popover().within(() => cy.findByText("Edit this collection").click());
           modal().within(() => {
             cy.findByLabelText("Name") /* [1] */
               .click()
@@ -165,8 +164,8 @@ describe("personal collections", () => {
             "should be able to archive collection(s) inside personal collection (metabase#15343)",
           );
 
-          cy.icon("pencil").click(); /* [4] */
-          cy.findByText("Archive this collection").click();
+          openCollectionMenu();
+          popover().within(() => cy.findByText("Archive").click());
           modal()
             .findByRole("button", { name: "Archive" })
             .click();
diff --git a/frontend/test/metabase/scenarios/models/models.cy.spec.js b/frontend/test/metabase/scenarios/models/models.cy.spec.js
index e3222f1a0c0..782618dcd6e 100644
--- a/frontend/test/metabase/scenarios/models/models.cy.spec.js
+++ b/frontend/test/metabase/scenarios/models/models.cy.spec.js
@@ -2,9 +2,7 @@ import {
   restore,
   modal,
   popover,
-  getNotebookStep,
   openNativeEditor,
-  openNewCollectionItemFlowFor,
   visualize,
   mockSessionProperty,
   sidebar,
@@ -393,83 +391,6 @@ describe("scenarios > models", () => {
     });
   });
 
-  describe("adding a question to collection from its page", () => {
-    it("should offer to pick one of the collection's models by default", () => {
-      cy.request("PUT", "/api/card/1", { dataset: true });
-      cy.request("PUT", "/api/card/2", { dataset: true });
-
-      cy.visit("/collection/root");
-      openNewCollectionItemFlowFor("question");
-
-      cy.findByText("Orders");
-      cy.findByText("Orders, Count");
-      cy.findByText("All data");
-
-      cy.findByText("Models").should("not.exist");
-      cy.findByText("Raw Data").should("not.exist");
-      cy.findByText("Saved Questions").should("not.exist");
-      cy.findByText("Sample Database").should("not.exist");
-
-      cy.findByText("Orders").click();
-
-      getNotebookStep("data").within(() => {
-        cy.findByText("Orders");
-      });
-
-      cy.button("Visualize");
-    });
-
-    it("should open the default picker after clicking 'All data'", () => {
-      cy.request("PUT", "/api/card/1", { dataset: true });
-      cy.request("PUT", "/api/card/2", { dataset: true });
-
-      cy.visit("/collection/root");
-      openNewCollectionItemFlowFor("question");
-
-      cy.findByText("All data").click({ force: true });
-
-      cy.findByText("Models");
-      cy.findByText("Raw Data");
-      cy.findByText("Saved Questions");
-    });
-
-    it("should automatically use the only collection model as a data source", () => {
-      cy.request("PUT", "/api/card/2", { dataset: true });
-
-      cy.visit("/collection/root");
-      openNewCollectionItemFlowFor("question");
-
-      getNotebookStep("data").within(() => {
-        cy.findByText("Orders, Count");
-      });
-      cy.button("Visualize");
-    });
-
-    it("should use correct picker if collection has no models", () => {
-      cy.request("PUT", "/api/card/1", { dataset: true });
-
-      cy.visit("/collection/9");
-      openNewCollectionItemFlowFor("question");
-
-      cy.findByText("All data").should("not.exist");
-      cy.findByText("Models");
-      cy.findByText("Raw Data");
-      cy.findByText("Saved Questions");
-    });
-
-    it("should use correct picker if there are models at all", () => {
-      cy.visit("/collection/root");
-      openNewCollectionItemFlowFor("question");
-
-      cy.findByText("All data").should("not.exist");
-      cy.findByText("Models").should("not.exist");
-      cy.findByText("Raw Data").should("not.exist");
-
-      cy.findByText("Saved Questions");
-      cy.findByText("Sample Database");
-    });
-  });
-
   it("shouldn't allow to turn native questions with variables into models", () => {
     cy.createNativeQuestion(
       {
diff --git a/frontend/test/metabase/scenarios/organization/moderation-collection.cy.spec.js b/frontend/test/metabase/scenarios/organization/moderation-collection.cy.spec.js
index f2fc88895be..2dfbc43b8a7 100644
--- a/frontend/test/metabase/scenarios/organization/moderation-collection.cy.spec.js
+++ b/frontend/test/metabase/scenarios/organization/moderation-collection.cy.spec.js
@@ -7,6 +7,7 @@ import {
   appBar,
   navigationSidebar,
   closeNavigationSidebar,
+  getCollectionActions,
 } from "__support__/e2e/cypress";
 import { SAMPLE_DATABASE } from "__support__/e2e/cypress_sample_database";
 
@@ -129,7 +130,9 @@ describeEE("collections types", () => {
     cy.visit("/collection/root");
 
     openCollection("Your personal collection");
-    cy.icon("pencil").should("not.exist");
+    getCollectionActions().within(() => {
+      cy.icon("ellipsis").should("not.exist");
+    });
 
     openNewCollectionItemFlowFor("collection");
     modal().within(() => {
@@ -276,7 +279,7 @@ function openCollection(collectionName) {
 }
 
 function editCollection() {
-  cy.icon("pencil").click();
+  cy.findByTestId("collection-menu").within(() => cy.icon("ellipsis").click());
   cy.findByText("Edit this collection").click();
 }
 
@@ -313,7 +316,9 @@ function createAndOpenOfficialCollection({ name }) {
     setOfficial();
     cy.button("Create").click();
   });
-  cy.findByText(name).click();
+  navigationSidebar().within(() => {
+    cy.findByText(name).click();
+  });
 }
 
 function changeCollectionTypeTo(type) {
diff --git a/frontend/test/metabase/scenarios/organization/timelines-collection.cy.spec.js b/frontend/test/metabase/scenarios/organization/timelines-collection.cy.spec.js
index 07a19bd4bb2..feb150e8d41 100644
--- a/frontend/test/metabase/scenarios/organization/timelines-collection.cy.spec.js
+++ b/frontend/test/metabase/scenarios/organization/timelines-collection.cy.spec.js
@@ -3,6 +3,7 @@ import {
   enableTracking,
   expectGoodSnowplowEvents,
   expectNoBadSnowplowEvents,
+  openCollectionMenu,
   resetSnowplow,
   restore,
 } from "__support__/e2e/cypress";
@@ -492,7 +493,7 @@ describe("scenarios > organization > timelines > collection", () => {
       cy.wait("@createTimeline");
       cy.icon("close").click();
 
-      cy.icon("pencil").click();
+      openCollectionMenu();
       cy.findByText("Edit this collection").click();
       cy.findByLabelText("Name")
         .clear()
diff --git a/frontend/test/metabase/scenarios/permissions/reproductions/22447-illogical-UI-elements-for-nodata.cy.spec.js b/frontend/test/metabase/scenarios/permissions/reproductions/22447-illogical-UI-elements-for-nodata.cy.spec.js
index a3814dd4221..8c987aab73e 100644
--- a/frontend/test/metabase/scenarios/permissions/reproductions/22447-illogical-UI-elements-for-nodata.cy.spec.js
+++ b/frontend/test/metabase/scenarios/permissions/reproductions/22447-illogical-UI-elements-for-nodata.cy.spec.js
@@ -33,10 +33,7 @@ describe("UI elements that make no sense for users without data permissions (met
     });
 
     cy.visit("/collection/root");
-
-    cy.get("main")
-      .find(".Icon-add")
-      .click();
+    cy.findByText("New").click();
 
     popover()
       .should("contain", "Dashboard")
@@ -70,10 +67,7 @@ describe("UI elements that make no sense for users without data permissions (met
       cy.icon("refresh").should("not.exist");
     });
     cy.visit("/collection/root");
-
-    cy.get("main")
-      .find(".Icon-add")
-      .click();
+    cy.findByText("New").click();
 
     popover()
       .should("contain", "Dashboard")
-- 
GitLab