diff --git a/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.jsx b/frontend/src/metabase/collections/components/CollectionCardVisualization/CollectionCardVisualization.jsx index 3c97b69fb11cf12b742a0bd7c82f549e336e5e22..2edc29b4c3aec106f72eff9cda55657ca99ad59c 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 96f5e0f87d88e2614d806d3e90459fa870e2118a..10e23b8d3e271cda660052598ef5be388ff483cb 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 5586b5398c95c1bbc2c8699093f25171a6716cd8..9be8de8c3b6978b91ffc22848b9b58763e90e8ae 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 0000000000000000000000000000000000000000..d17c3d71cb24299d974ed712c08d5a4d78a13a5f --- /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 0000000000000000000000000000000000000000..ccee178ab4c733dd9ac1d9ba0be35c4a4e460dc5 --- /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 0000000000000000000000000000000000000000..d03b81e03b11e384e7c4abd8b7e3132f46e69439 --- /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 0519f390d522db8bb77f8e0bcc8d85b980478d79..0a34498d17103e9595e756fb6584205bf720d579 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 fed39dbc5346a0a69b8318530137fef6d6b6bead..f66d6478be39cdba523b72b2402549630fe72ef7 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 528c4c39b6e376ad83924303e576b1ace37f6f62..25959bf2f14339a770e6357f805035a3f5c4cdde 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 0000000000000000000000000000000000000000..3316c46814b6f721e488b363ec35ebe76b810d9e --- /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 0000000000000000000000000000000000000000..ed6411ca6c2a52331ec34d34fc556a3d17ac0257 --- /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 0000000000000000000000000000000000000000..e884f94f78930d279fe7ff7cc414b3e4283d9277 --- /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 6a821a16afdcbc56ba018c539c9aac901529dc70..37ca39ad845ebd3a33dbd2d30385330357222ef7 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 93a882d482c19364926b301ac85cca87c7a25d77..7f6cc48b8300afdeb07e1ba204c85f7be9f405b6 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 50248400ce3b1f3bf791689600af575af6d576e2..c5d89a564e3295300ad1b08d267de53ff252a4d3 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 88ea922d3fae36c9160cb9819eb70e4e32b8c228..52215b840798364ccda10a56c597ed829b7ae78c 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 40ebcf7ac753ea5143b3b6a82ffabadb6658c93a..372b37e34985e56b0774938240e4a2e0eea12d7a 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 0000000000000000000000000000000000000000..e4335e270a8733431d9cb76a85091e60900bd260 --- /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 b6c1665c7dfe5e3ba2ab01e45c30ebed499f1b51..be3ffa6f305d6674850086e563b78d91fb8cdc15 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 = [