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