From 65e72dc549dce4a54f21edf3cc87e7f3e2354c33 Mon Sep 17 00:00:00 2001 From: Dalton <daltojohnso@users.noreply.github.com> Date: Mon, 31 Jan 2022 10:15:15 -0700 Subject: [PATCH] Add drag and drop reordering of pinned items (#19966) * onDrop unneeded * basic impl of reordering pinned items * split cards and dashboards again * Improve pin drop target styling * Fix card drop target weirdness * brand-light border --> brand * pull out separate styled drop target component * replace style prop usage * reduce the z-index * Remove prop that triggers drill functionality * Fix clobbering of pinned list on update * Fix card queries on every rerender * Add pin drop zone & split drop target components * lint fix * fix & add a few tests * update pin drop zone styling to match sorting --- .../CollectionCardVisualization.jsx | 12 +- .../CollectionCardVisualization.styled.jsx | 5 + .../collections/components/ItemsTable.jsx | 35 ++--- .../PinDropZone/PinDropZone.styled.tsx | 42 ++++++ .../components/PinDropZone/PinDropZone.tsx | 30 ++++ .../components/PinDropZone/index.ts | 1 + .../PinnedItemOverview.styled.tsx | 7 +- .../PinnedItemOverview/PinnedItemOverview.tsx | 129 +++++++++++------- .../PinnedItemOverview.unit.spec.js | 40 +++++- .../PinnedItemSortDropTarget.styled.tsx | 50 +++++++ .../PinnedItemSortDropTarget.tsx | 17 +++ .../PinnedItemSortDropTarget/index.ts | 1 + .../containers/CollectionContent.jsx | 19 ++- .../src/metabase/containers/dnd/DropArea.jsx | 5 +- .../containers/dnd/ItemDragSource.jsx | 2 +- .../containers/dnd/ItemsDragLayer.jsx | 1 + .../metabase/containers/dnd/PinDropTarget.jsx | 16 ++- .../dnd/PinnedItemSortDropTarget.jsx | 56 ++++++++ frontend/src/metabase/containers/dnd/index.js | 1 + 19 files changed, 368 insertions(+), 101 deletions(-) create mode 100644 frontend/src/metabase/collections/components/PinDropZone/PinDropZone.styled.tsx create mode 100644 frontend/src/metabase/collections/components/PinDropZone/PinDropZone.tsx create mode 100644 frontend/src/metabase/collections/components/PinDropZone/index.ts create mode 100644 frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.styled.tsx create mode 100644 frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.tsx create mode 100644 frontend/src/metabase/collections/components/PinnedItemSortDropTarget/index.ts create mode 100644 frontend/src/metabase/containers/dnd/PinnedItemSortDropTarget.jsx diff --git a/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.jsx b/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.jsx index 3c97b69fb11..2edc29b4c3a 100644 --- a/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.jsx +++ b/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useRef } from "react"; import PropTypes from "prop-types"; import _ from "underscore"; @@ -29,6 +29,8 @@ function CollectionCardVisualization({ onCopy, onMove, }) { + const questionRef = useRef(); + return ( <ItemLink to={item.getUrl()}> <VizCard flat> @@ -40,9 +42,12 @@ function CollectionCardVisualization({ /> <Questions.Loader id={item.id}> {({ question: card }) => { - const question = new Question(card, metadata); + // reusing the initial question instance avoids triggering queries every time this component rerenders + questionRef.current = + questionRef.current || new Question(card, metadata); + return ( - <QuestionResultLoader question={question}> + <QuestionResultLoader question={questionRef.current}> {({ loading, error, reload, rawSeries, results, result }) => { const shouldShowLoader = loading && results == null; const { errorMessage, errorIcon } = getErrorProps( @@ -56,7 +61,6 @@ function CollectionCardVisualization({ noWrapper > <Visualization - onChangeCardAndRun={_.noop} isDashboard showTitle metadata={metadata} diff --git a/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.styled.jsx b/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.styled.jsx index 96f5e0f87d8..10e23b8d3e2 100644 --- a/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.styled.jsx +++ b/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.styled.jsx @@ -3,6 +3,7 @@ import styled from "styled-components"; import ActionMenu from "metabase/collections/components/ActionMenu"; import Card from "metabase/components/Card"; import { color } from "metabase/lib/colors"; +import { LegendLabel } from "metabase/visualizations/components/legend/LegendCaption.styled"; const HEIGHT = 250; @@ -26,5 +27,9 @@ export const VizCard = styled(Card)` ${HoverMenu} { visibility: visible; } + + ${LegendLabel} { + color: ${color("brand")}; + } } `; diff --git a/frontend/src/metabase/collections/components/ItemsTable.jsx b/frontend/src/metabase/collections/components/ItemsTable.jsx index 5586b5398c9..9be8de8c3b6 100644 --- a/frontend/src/metabase/collections/components/ItemsTable.jsx +++ b/frontend/src/metabase/collections/components/ItemsTable.jsx @@ -1,34 +1,12 @@ import React from "react"; import PropTypes from "prop-types"; -import { t } from "ttag"; import { Flex } from "grid-styled"; -import { color } from "metabase/lib/colors"; - -import PinDropTarget from "metabase/containers/dnd/PinDropTarget"; - import { ANALYTICS_CONTEXT } from "metabase/collections/constants"; +import PinDropZone from "metabase/collections/components/PinDropZone"; import BaseItemsTable from "./BaseItemsTable"; -function ItemsEmptyState() { - return ( - <PinDropTarget pinIndex={null} hideUntilDrag margin={10}> - {({ hovered }) => ( - <Flex - align="center" - justify="center" - py={2} - m={2} - color={hovered ? color("brand") : color("text-medium")} - > - {t`Drag here to un-pin`} - </Flex> - )} - </PinDropTarget> - ); -} - Item.propTypes = { item: PropTypes.object.isRequired, }; @@ -55,13 +33,18 @@ function ItemsTable(props) { const { items } = props; if (items.length === 0) { - return <ItemsEmptyState />; + return ( + <Flex className="relative" align="center" justify="center" p={4} m={2}> + <PinDropZone variant="unpin" /> + </Flex> + ); } return ( - <PinDropTarget pinIndex={null}> + <div className="relative"> + <PinDropZone variant="unpin" /> <BaseItemsTable {...props} renderItem={Item} /> - </PinDropTarget> + </div> ); } diff --git a/frontend/src/metabase/collections/components/PinDropZone/PinDropZone.styled.tsx b/frontend/src/metabase/collections/components/PinDropZone/PinDropZone.styled.tsx new file mode 100644 index 00000000000..d17c3d71cb2 --- /dev/null +++ b/frontend/src/metabase/collections/components/PinDropZone/PinDropZone.styled.tsx @@ -0,0 +1,42 @@ +import styled from "styled-components"; + +import PinDropTarget from "metabase/containers/dnd/PinDropTarget"; +import { color } from "metabase/lib/colors"; + +export type PinDropTargetProps = { + variant: "pin" | "unpin"; + pinIndex: number | null; + hideUntilDrag: boolean; +}; + +export type PinDropTargetRenderArgs = PinDropTargetProps & { + hovered: boolean; + highlighted: boolean; +}; + +export const StyledPinDropTarget = styled(PinDropTarget)<PinDropTargetProps>` + position: absolute !important; + top: 0; + bottom: 0; + left: -1rem; + right: -1rem; + pointer-events: none; + background-color: transparent !important; + + * { + pointer-events: all; + background-color: transparent !important; + } +`; + +export const PinDropTargetIndicator = styled.div<PinDropTargetRenderArgs>` + z-index: 1; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-left: ${props => + `4px solid ${props.hovered ? color("brand") : color("bg-medium")}`}; + display: ${props => !(props.hovered || props.highlighted) && "none"}; +`; diff --git a/frontend/src/metabase/collections/components/PinDropZone/PinDropZone.tsx b/frontend/src/metabase/collections/components/PinDropZone/PinDropZone.tsx new file mode 100644 index 00000000000..ccee178ab4c --- /dev/null +++ b/frontend/src/metabase/collections/components/PinDropZone/PinDropZone.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import PropTypes from "prop-types"; + +import { + StyledPinDropTarget, + PinDropTargetIndicator, + PinDropTargetProps, + PinDropTargetRenderArgs, +} from "./PinDropZone.styled"; + +type PinDropZoneProps = Pick<PinDropTargetProps, "variant">; + +PinDropZone.propTypes = { + variant: PropTypes.oneOf(["pin", "unpin"]).isRequired, +}; + +function PinDropZone({ variant, ...props }: PinDropZoneProps) { + return ( + <StyledPinDropTarget + variant={variant} + pinIndex={variant === "pin" ? 1 : null} + hideUntilDrag + {...props} + > + {(args: PinDropTargetRenderArgs) => <PinDropTargetIndicator {...args} />} + </StyledPinDropTarget> + ); +} + +export default PinDropZone; diff --git a/frontend/src/metabase/collections/components/PinDropZone/index.ts b/frontend/src/metabase/collections/components/PinDropZone/index.ts new file mode 100644 index 00000000000..d03b81e03b1 --- /dev/null +++ b/frontend/src/metabase/collections/components/PinDropZone/index.ts @@ -0,0 +1 @@ +export { default } from "./PinDropZone"; diff --git a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.styled.tsx b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.styled.tsx index 0519f390d52..0a34498d171 100644 --- a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.styled.tsx +++ b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.styled.tsx @@ -3,16 +3,19 @@ import styled from "styled-components"; import { color } from "metabase/lib/colors"; import { breakpointMaxMedium } from "metabase/styled-components/theme"; +export const GAP_REM = 1.15; + export const Container = styled.div` + position: relative; display: flex; flex-direction: column; - gap: 1.15rem; + gap: ${GAP_REM}rem; `; export const Grid = styled.div` display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 1.15rem; + gap: ${GAP_REM}rem; ${breakpointMaxMedium} { grid-template-columns: minmax(0, 1fr); diff --git a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx index fed39dbc534..f66d6478be3 100644 --- a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx +++ b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.tsx @@ -6,7 +6,9 @@ import Metadata from "metabase-lib/lib/metadata/Metadata"; import PinnedItemCard from "metabase/collections/components/PinnedItemCard"; import CollectionCardVisualization from "metabase/collections/components/CollectionCardVisualization"; import EmptyPinnedItemsBanner from "../EmptyPinnedItemsBanner/EmptyPinnedItemsBanner"; +import PinnedItemSortDropTarget from "metabase/collections/components/PinnedItemSortDropTarget"; import { Item, Collection, isRootCollection } from "metabase/collections/utils"; +import PinDropZone from "metabase/collections/components/PinDropZone"; import ItemDragSource from "metabase/containers/dnd/ItemDragSource"; import { @@ -22,7 +24,6 @@ type Props = { metadata: Metadata; onCopy: (items: Item[]) => void; onMove: (items: Item[]) => void; - onDrop: () => void; }; function PinnedItemOverview({ @@ -31,9 +32,8 @@ function PinnedItemOverview({ metadata, onCopy, onMove, - onDrop, }: Props) { - const sortedItems = _.sortBy(items, item => item.name); + const sortedItems = _.sortBy(items, item => item.collection_position); const { card: cardItems = [], dashboard: dashboardItems = [], @@ -42,53 +42,75 @@ function PinnedItemOverview({ return items.length === 0 ? ( <Container> + <PinDropZone variant="pin" /> <EmptyPinnedItemsBanner /> </Container> ) : ( <Container data-testid="pinned-items"> + <PinDropZone variant="pin" /> {cardItems.length > 0 && ( <Grid> {cardItems.map(item => ( - <ItemDragSource - key={item.id} - item={item} - collection={collection} - onDrop={onDrop} - > - <div> - <CollectionCardVisualization - item={item} - collection={collection} - metadata={metadata} - onCopy={onCopy} - onMove={onMove} - /> - </div> - </ItemDragSource> + <div key={item.id} className="relative"> + <PinnedItemSortDropTarget + isFrontTarget + itemModel="card" + pinIndex={item.collection_position} + enableDropTargetBackground={false} + /> + <ItemDragSource item={item} collection={collection}> + <div> + <CollectionCardVisualization + item={item} + collection={collection} + metadata={metadata} + onCopy={onCopy} + onMove={onMove} + /> + </div> + </ItemDragSource> + <PinnedItemSortDropTarget + isBackTarget + itemModel="card" + pinIndex={item.collection_position} + enableDropTargetBackground={false} + /> + </div> ))} </Grid> )} + {dashboardItems.length > 0 && ( <Grid> {dashboardItems.map(item => ( - <ItemDragSource - key={item.id} - item={item} - collection={collection} - onDrop={onDrop} - > - <div> - <PinnedItemCard - item={item} - collection={collection} - onCopy={onCopy} - onMove={onMove} - /> - </div> - </ItemDragSource> + <div key={item.id} className="relative"> + <PinnedItemSortDropTarget + isFrontTarget + itemModel="dashboard" + pinIndex={item.collection_position} + enableDropTargetBackground={false} + /> + <ItemDragSource item={item} collection={collection}> + <div> + <PinnedItemCard + item={item} + collection={collection} + onCopy={onCopy} + onMove={onMove} + /> + </div> + </ItemDragSource> + <PinnedItemSortDropTarget + isBackTarget + itemModel="dashboard" + pinIndex={item.collection_position} + enableDropTargetBackground={false} + /> + </div> ))} </Grid> )} + {dataModelItems.length > 0 && ( <div> <SectionHeader> @@ -101,21 +123,30 @@ function PinnedItemOverview({ </SectionHeader> <Grid> {dataModelItems.map(item => ( - <ItemDragSource - key={item.id} - item={item} - collection={collection} - onDrop={onDrop} - > - <div> - <PinnedItemCard - item={item} - collection={collection} - onCopy={onCopy} - onMove={onMove} - /> - </div> - </ItemDragSource> + <div key={item.id} className="relative"> + <PinnedItemSortDropTarget + isFrontTarget + itemModel="dataset" + pinIndex={item.collection_position} + enableDropTargetBackground={false} + /> + <ItemDragSource item={item} collection={collection}> + <div> + <PinnedItemCard + item={item} + collection={collection} + onCopy={onCopy} + onMove={onMove} + /> + </div> + </ItemDragSource> + <PinnedItemSortDropTarget + isBackTarget + itemModel="dataset" + pinIndex={item.collection_position} + enableDropTargetBackground={false} + /> + </div> ))} </Grid> </div> diff --git a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.unit.spec.js b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.unit.spec.js index 528c4c39b6e..25959bf2f14 100644 --- a/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.unit.spec.js +++ b/frontend/src/metabase/collections/components/PinnedItemOverview/PinnedItemOverview.unit.spec.js @@ -1,5 +1,5 @@ import React from "react"; -import { render } from "@testing-library/react"; +import { renderWithProviders, screen } from "__support__/ui"; import PinnedItemOverview from "./PinnedItemOverview"; @@ -13,26 +13,38 @@ const defaultCollection = { archived: false, }; -const defaultItem = { +const dashboardItem1 = { id: 1, model: "dashboard", - collection_position: 1, + collection_position: 2, name: "Dashboard Foo", - description: "description foo foo foo", + description: "description foo", getIcon: () => ({ name: "dashboard" }), getUrl: () => "/dashboard/1", setArchived: jest.fn(), setPinned: jest.fn(), }; +const dashboardItem2 = { + id: 2, + model: "dashboard", + collection_position: 1, + name: "Dashboard Bar", + description: "description foo", + getIcon: () => ({ name: "dashboard" }), + getUrl: () => "/dashboard/2", + setArchived: jest.fn(), + setPinned: jest.fn(), +}; + function setup({ items, collection } = {}) { - items = items || [defaultItem]; + items = items || [dashboardItem1, dashboardItem2]; collection = collection || defaultCollection; mockOnCopy.mockReset(); mockOnMove.mockReset(); - return render( + return renderWithProviders( <PinnedItemOverview items={items} collection={collection} @@ -41,6 +53,9 @@ function setup({ items, collection } = {}) { onMove={mockOnMove} onDrop={jest.fn()} />, + { + withDND: true, + }, ); } @@ -51,4 +66,17 @@ describe("PinnedItemOverview", () => { "Save your questions, dashboards, and models in collections — and pin them to feature them at the top.", ); }); + + it("should render items", () => { + setup(); + expect(screen.getByText(dashboardItem1.name)).toBeInTheDocument(); + expect(screen.getByText(dashboardItem2.name)).toBeInTheDocument(); + }); + + it("should render items sorted by collection_position", () => { + setup(); + const names = screen.queryAllByText(/Dashboard (Foo|Bar)/); + expect(names[0].textContent).toContain(dashboardItem2.name); + expect(names[1].textContent).toContain(dashboardItem1.name); + }); }); diff --git a/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.styled.tsx b/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.styled.tsx new file mode 100644 index 00000000000..3316c46814b --- /dev/null +++ b/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.styled.tsx @@ -0,0 +1,50 @@ +import styled from "styled-components"; + +import PinnedItemSortDropTarget from "metabase/containers/dnd/PinnedItemSortDropTarget"; +import { GAP_REM } from "metabase/collections/components/PinnedItemOverview/PinnedItemOverview.styled"; +import { color } from "metabase/lib/colors"; + +export type PinDropTargetProps = { + isBackTarget?: boolean; + isFrontTarget?: boolean; + itemModel: string; + pinIndex?: number | null; + enableDropTargetBackground?: boolean; +}; + +export type PinDropTargetRenderArgs = PinDropTargetProps & { + hovered: boolean; + highlighted: boolean; +}; + +export const StyledPinDropTarget = styled<PinDropTargetProps>( + PinnedItemSortDropTarget, +)` + position: absolute !important; + top: 0; + bottom: 0; + left: -${(GAP_REM * 5) / 8}rem; + right: -${(GAP_REM * 5) / 8}rem; + pointer-events: none; + background-color: transparent; + + * { + pointer-events: all; + } +`; + +export const PinDropTargetIndicator = styled.div<PinDropTargetRenderArgs>` + z-index: 1; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + border-left: ${props => + props.isFrontTarget && + `4px solid ${props.hovered ? color("brand") : color("bg-medium")}`};} + border-right: ${props => + props.isBackTarget && + `4px solid ${props.hovered ? color("brand") : color("bg-medium")}`};} + display: ${props => !(props.hovered || props.highlighted) && "none"}; +`; diff --git a/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.tsx b/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.tsx new file mode 100644 index 00000000000..ed6411ca6c2 --- /dev/null +++ b/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/PinnedItemSortDropTarget.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { + StyledPinDropTarget, + PinDropTargetIndicator, + PinDropTargetProps, + PinDropTargetRenderArgs, +} from "./PinnedItemSortDropTarget.styled"; + +function PinnedItemSortDropTarget(props: PinDropTargetProps) { + return ( + <StyledPinDropTarget {...props}> + {(args: PinDropTargetRenderArgs) => <PinDropTargetIndicator {...args} />} + </StyledPinDropTarget> + ); +} + +export default PinnedItemSortDropTarget; diff --git a/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/index.ts b/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/index.ts new file mode 100644 index 00000000000..e884f94f789 --- /dev/null +++ b/frontend/src/metabase/collections/components/PinnedItemSortDropTarget/index.ts @@ -0,0 +1 @@ +export { default } from "./PinnedItemSortDropTarget"; diff --git a/frontend/src/metabase/collections/containers/CollectionContent.jsx b/frontend/src/metabase/collections/containers/CollectionContent.jsx index 6a821a16afd..37ca39ad845 100644 --- a/frontend/src/metabase/collections/containers/CollectionContent.jsx +++ b/frontend/src/metabase/collections/containers/CollectionContent.jsx @@ -150,17 +150,14 @@ function CollectionContent({ )} handleToggleMobileSidebar={handleToggleMobileSidebar} /> - {!loadingPinnedItems && ( - <PinnedItemOverview - items={pinnedItems} - collection={collection} - metadata={metadata} - onMove={handleMove} - onCopy={handleCopy} - onToggleSelected={toggleItem} - onDrop={clear} - /> - )} + <PinnedItemOverview + items={pinnedItems} + collection={collection} + metadata={metadata} + onMove={handleMove} + onCopy={handleCopy} + onToggleSelected={toggleItem} + /> <Search.ListLoader query={unpinnedQuery} loadingAndErrorWrapper={false} diff --git a/frontend/src/metabase/containers/dnd/DropArea.jsx b/frontend/src/metabase/containers/dnd/DropArea.jsx index 93a882d482c..7f6cc48b830 100644 --- a/frontend/src/metabase/containers/dnd/DropArea.jsx +++ b/frontend/src/metabase/containers/dnd/DropArea.jsx @@ -55,13 +55,16 @@ export default class DropArea extends React.Component { children, className, style, + enableDropTargetBackground = true, ...props } = this.props; return this.state.show ? connectDropTarget( <div className={cx("relative", className)} style={style}> {typeof children === "function" ? children(props) : children} - <DropTargetBackgroundAndBorder {...props} /> + {enableDropTargetBackground && ( + <DropTargetBackgroundAndBorder {...props} /> + )} </div>, ) : null; diff --git a/frontend/src/metabase/containers/dnd/ItemDragSource.jsx b/frontend/src/metabase/containers/dnd/ItemDragSource.jsx index 50248400ce3..c5d89a564e3 100644 --- a/frontend/src/metabase/containers/dnd/ItemDragSource.jsx +++ b/frontend/src/metabase/containers/dnd/ItemDragSource.jsx @@ -43,7 +43,7 @@ import { dragTypeForItem } from "."; onDrop && onDrop(); } catch (e) { - alert("There was a problem moving these items: " + e); + console.error("There was a problem moving these items: " + e); } } }, diff --git a/frontend/src/metabase/containers/dnd/ItemsDragLayer.jsx b/frontend/src/metabase/containers/dnd/ItemsDragLayer.jsx index 88ea922d3fa..52215b84079 100644 --- a/frontend/src/metabase/containers/dnd/ItemsDragLayer.jsx +++ b/frontend/src/metabase/containers/dnd/ItemsDragLayer.jsx @@ -43,6 +43,7 @@ export default class ItemsDragLayer extends React.Component { transform: `translate(${x}px, ${y}px)`, pointerEvents: "none", opacity: 0.65, + zIndex: 1, }} > <DraggedItems diff --git a/frontend/src/metabase/containers/dnd/PinDropTarget.jsx b/frontend/src/metabase/containers/dnd/PinDropTarget.jsx index 40ebcf7ac75..372b37e3498 100644 --- a/frontend/src/metabase/containers/dnd/PinDropTarget.jsx +++ b/frontend/src/metabase/containers/dnd/PinDropTarget.jsx @@ -1,4 +1,7 @@ import { DropTarget } from "react-dnd"; +import PropTypes from "prop-types"; + +import { isItemPinned } from "metabase/collections/utils"; import DropArea from "./DropArea"; import { PinnableDragTypes } from "."; @@ -13,9 +16,16 @@ const PinDropTarget = DropTarget( }, canDrop(props, monitor) { const { item } = monitor.getItem(); + const { variant } = props; // NOTE: not necessary to check collection permission here since we // enforce it when beginning to drag and item within the same collection - return props.pinIndex !== item.collection_position; + if (variant === "pin") { + return !isItemPinned(item); + } else if (variant === "unpin") { + return isItemPinned(item); + } + + return false; }, }, (connect, monitor) => ({ @@ -25,4 +35,8 @@ const PinDropTarget = DropTarget( }), )(DropArea); +PinDropTarget.propTypes = { + variant: PropTypes.oneOf(["pin", "unpin"]).isRequired, +}; + export default PinDropTarget; diff --git a/frontend/src/metabase/containers/dnd/PinnedItemSortDropTarget.jsx b/frontend/src/metabase/containers/dnd/PinnedItemSortDropTarget.jsx new file mode 100644 index 00000000000..e4335e270a8 --- /dev/null +++ b/frontend/src/metabase/containers/dnd/PinnedItemSortDropTarget.jsx @@ -0,0 +1,56 @@ +import { DropTarget } from "react-dnd"; +import PropTypes from "prop-types"; + +import { isItemPinned } from "metabase/collections/utils"; + +import DropArea from "./DropArea"; +import { PinnableDragTypes } from "."; + +const PinnedItemSortDropTarget = DropTarget( + PinnableDragTypes, + { + drop(props, monitor, component) { + if (!props.noDrop) { + return { pinIndex: props.pinIndex }; + } + }, + canDrop(props, monitor) { + const { item } = monitor.getItem(); + const { isFrontTarget, isBackTarget, itemModel, pinIndex } = props; + + // NOTE: not necessary to check collection permission here since we + // enforce it when beginning to drag and item within the same collection + if (!isItemPinned(item)) { + return false; + } + + if (itemModel != null && item.model !== itemModel) { + return false; + } + + if (isFrontTarget) { + const isInFrontOfItem = pinIndex < item.collection_position; + return isInFrontOfItem; + } else if (isBackTarget) { + const isBehindItem = pinIndex > item.collection_position; + return isBehindItem; + } + + return false; + }, + }, + (connect, monitor) => ({ + highlighted: monitor.canDrop(), + hovered: monitor.isOver() && monitor.canDrop(), + connectDropTarget: connect.dropTarget(), + }), +)(DropArea); + +PinnedItemSortDropTarget.propTypes = { + isFrontTarget: PropTypes.bool, + isBackTarget: PropTypes.bool, + itemModel: PropTypes.string, + pinIndex: PropTypes.number, +}; + +export default PinnedItemSortDropTarget; diff --git a/frontend/src/metabase/containers/dnd/index.js b/frontend/src/metabase/containers/dnd/index.js index b6c1665c7df..be3ffa6f305 100644 --- a/frontend/src/metabase/containers/dnd/index.js +++ b/frontend/src/metabase/containers/dnd/index.js @@ -15,6 +15,7 @@ export const PinnableDragTypes = [ DragTypes.QUESTION, DragTypes.DASHBOARD, DragTypes.PULSE, + DragTypes.DATASET, ]; export const MoveableDragTypes = [ -- GitLab