Skip to content
Snippets Groups Projects
Commit 3bb090f0 authored by Tom Robinson's avatar Tom Robinson
Browse files

Merge branch 'master' of github.com:metabase/metabase into refactor-settings

parents 04eda916 8b8c7e57
No related branches found
No related tags found
No related merge requests found
Showing
with 598 additions and 169 deletions
{
"trailing-comma": "all"
"trailingComma": "all"
}
......@@ -15,6 +15,7 @@ import {
isString,
isSummable,
isCategory,
isLocation,
isDimension,
isMetric,
isPK,
......@@ -59,6 +60,9 @@ export default class Field extends Base {
isString() {
return isString(this);
}
isLocation() {
return isLocation(this);
}
isSummable() {
return isSummable(this);
}
......
......@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
import FilterList from "metabase/query_builder/components/filters/FilterList.jsx";
import FilterList from "metabase/query_builder/components/FilterList.jsx";
import AggregationWidget from "metabase/query_builder/components/AggregationWidget.jsx";
import Query from "metabase/lib/query";
......@@ -31,11 +31,7 @@ export default class QueryDiff extends Component {
/>
)}
{filters.length > 0 && (
<FilterList
filters={filters}
tableMetadata={tableMetadata}
maxDisplayValues={Infinity}
/>
<FilterList filters={filters} maxDisplayValues={Infinity} />
)}
</div>
)}
......
......@@ -656,7 +656,7 @@ export const getDatabasesPermissionsGrid = createSelector(
},
);
import Collections, { getCollectionsById } from "metabase/entities/collections";
import Collections from "metabase/entities/collections";
const getCollectionId = (state, props) => props && props.collectionId;
const getSingleCollectionPermissionsMode = (state, props) =>
......@@ -664,23 +664,21 @@ const getSingleCollectionPermissionsMode = (state, props) =>
const getCollections = createSelector(
[
Collections.selectors.getList,
Collections.selectors.getExpandedCollectionsById,
getCollectionId,
getSingleCollectionPermissionsMode,
],
(collections, collectionId, singleMode) => {
if (!collections) {
return null;
}
const collectionsById = getCollectionsById(collections);
(collectionsById, collectionId, singleMode) => {
if (collectionId && collectionsById[collectionId]) {
if (singleMode) {
return [collectionsById[collectionId]];
} else {
return collectionsById[collectionId].children;
}
} else {
} else if (collectionsById["root"]) {
return [collectionsById["root"]];
} else {
return null;
}
},
);
......
......@@ -34,10 +34,15 @@ import { getStore } from "./store";
import { refreshSiteSettings } from "metabase/redux/settings";
// router
import { Router, useRouterHistory } from "react-router";
import { createHistory } from "history";
import { syncHistoryWithStore } from "react-router-redux";
// drag and drop
import HTML5Backend from "react-dnd-html5-backend";
import { DragDropContextProvider } from "react-dnd";
// remove trailing slash
const BASENAME = window.MetabaseRoot.replace(/\/+$/, "");
......@@ -58,9 +63,11 @@ function _init(reducers, getRoutes, callback) {
ReactDOM.render(
<Provider store={store}>
<ThemeProvider theme={theme}>
<Router history={history}>{routes}</Router>
</ThemeProvider>
<DragDropContextProvider backend={HTML5Backend} context={{ window }}>
<ThemeProvider theme={theme}>
<Router history={history}>{routes}</Router>
</ThemeProvider>
</DragDropContextProvider>
</Provider>,
document.getElementById("root"),
);
......
......@@ -7,10 +7,12 @@ import { normal as defaultColors } from "metabase/lib/colors";
export default class CheckBox extends Component {
static propTypes = {
checked: PropTypes.bool,
indeterminate: PropTypes.bool,
onChange: PropTypes.func,
color: PropTypes.oneOf(Object.keys(defaultColors)),
size: PropTypes.number, // TODO - this should probably be a concrete set of options
padding: PropTypes.number, // TODO - the component should pad itself properly based on the size
noIcon: PropTypes.bool,
};
static defaultProps = {
......@@ -31,15 +33,16 @@ export default class CheckBox extends Component {
}
render() {
const { checked, color, padding, size } = this.props;
const { checked, indeterminate, color, padding, size, noIcon } = this.props;
const themeColor = defaultColors[color];
const checkedColor = defaultColors[color];
const uncheckedColor = "#ddd";
const checkboxStyle = {
width: size,
height: size,
backgroundColor: checked ? themeColor : "white",
border: `2px solid ${checked ? themeColor : "#ddd"}`,
backgroundColor: checked ? checkedColor : "white",
border: `2px solid ${checked ? checkedColor : uncheckedColor}`,
};
return (
<div
......@@ -52,13 +55,14 @@ export default class CheckBox extends Component {
style={checkboxStyle}
className="flex align-center justify-center rounded"
>
{checked && (
<Icon
style={{ color: checked ? "white" : themeColor }}
name="check"
size={size - padding * 2}
/>
)}
{(checked || indeterminate) &&
!noIcon && (
<Icon
style={{ color: checked ? "white" : uncheckedColor }}
name={indeterminate ? "dash" : "check"}
size={size - padding * 2}
/>
)}
</div>
</div>
);
......
......@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import _ from "underscore";
import listSelect from "metabase/hoc/ListSelect";
import BulkActionBar from "metabase/components/BulkActionBar";
import cx from "classnames";
import * as Urls from "metabase/lib/urls";
import { normal } from "metabase/lib/colors";
......@@ -23,17 +24,23 @@ import Ellipsified from "metabase/components/Ellipsified";
import VirtualizedList from "metabase/components/VirtualizedList";
import BrowserCrumbs from "metabase/components/BrowserCrumbs";
import CollectionLoader from "metabase/containers/CollectionLoader";
import CollectionMoveModal from "metabase/containers/CollectionMoveModal";
import { entityListLoader } from "metabase/entities/containers/EntityListLoader";
import { entityObjectLoader } from "metabase/entities/containers/EntityObjectLoader";
import { ROOT_COLLECTION } from "metabase/entities/collections";
import Collections from "metabase/entities/collections";
// drag-and-drop components
import ItemDragSource from "metabase/containers/dnd/ItemDragSource";
import CollectionDropTarget from "metabase/containers/dnd/CollectionDropTarget";
import PinPositionDropTarget from "metabase/containers/dnd/PinPositionDropTarget";
import PinDropTarget from "metabase/containers/dnd/PinDropTarget";
import ItemsDragLayer from "metabase/containers/dnd/ItemsDragLayer";
const CollectionItem = ({ collection, iconName = "all" }) => (
const CollectionItem = ({ collection, color, iconName = "all" }) => (
<Link
to={`collection/${collection.id}`}
hover={{ color: normal.blue }}
color={normal.grey2}
color={color || normal.grey2}
>
<Flex align="center" py={1} key={`collection-${collection.id}`}>
<Icon name={iconName} mx={1} color="#93B3C9" />
......@@ -52,13 +59,19 @@ class CollectionList extends React.Component {
<Box mb={2}>
<Box my={2}>
{isRoot && (
<CollectionItem
collection={{
name: t`My personal collection`,
id: currentUser.personal_collection_id,
}}
iconName="star"
/>
<Box className="relative">
<CollectionDropTarget
collection={{ id: currentUser.personal_collection_id }}
>
<CollectionItem
collection={{
name: t`My personal collection`,
id: currentUser.personal_collection_id,
}}
iconName="star"
/>
</CollectionDropTarget>
</Box>
)}
{isRoot &&
currentUser.is_superuser && (
......@@ -77,8 +90,12 @@ class CollectionList extends React.Component {
{collections
.filter(c => c.id !== currentUser.personal_collection_id)
.map(collection => (
<Box key={collection.id} mb={1}>
<CollectionItem collection={collection} />
<Box key={collection.id} mb={1} className="relative">
<CollectionDropTarget collection={collection}>
<ItemDragSource item={collection}>
<CollectionItem collection={collection} />
</ItemDragSource>
</CollectionDropTarget>
</Box>
))}
</Box>
......@@ -88,12 +105,32 @@ class CollectionList extends React.Component {
const ROW_HEIGHT = 72;
import { entityListLoader } from "metabase/entities/containers/EntityListLoader";
@entityListLoader({
entityType: "search",
entityQuery: (state, props) => ({ collection: props.collectionId }),
wrapped: true,
})
@listSelect()
@connect((state, props) => {
// split out collections, pinned, and unpinned since bulk actions only apply to unpinned
const [collections, items] = _.partition(
props.list,
item => item.model === "collection",
);
const [pinned, unpinned] = _.partition(
items,
item => item.collection_position != null,
);
// sort the pinned items by collection_position
pinned.sort((a, b) => a.collection_position - b.collection_position);
return { collections, pinned, unpinned };
})
// only apply bulk actions to unpinned items
@listSelect({
listProp: "unpinned",
keyForItem: item => `${item.model}:${item.id}`,
})
class DefaultLanding extends React.Component {
state = {
moveItems: null,
......@@ -101,12 +138,17 @@ class DefaultLanding extends React.Component {
render() {
const {
collection,
collectionId,
list,
onToggleSelected,
collections,
pinned,
unpinned,
selected,
selection,
onToggleSelected,
onSelectNone,
reload,
} = this.props;
const { moveItems } = this.state;
......@@ -118,16 +160,6 @@ class DefaultLanding extends React.Component {
onSelectNone();
};
// exclude collections from selection since they can't currently be selected
const selected = this.props.selected.filter(
item => item.model !== "collection",
);
const [collections, items] = _.partition(
list,
item => item.entity_type === "collections",
);
// Show the
const showCollectionList =
collectionId === "root" || collections.length > 0;
......@@ -147,94 +179,112 @@ class DefaultLanding extends React.Component {
)}
<Box w={2 / 3}>
<Box>
<CollectionLoader collectionId={collectionId}>
{({ object: collection }) => {
if (items.length === 0) {
return <CollectionEmptyState />;
}
const [pinned, other] = _.partition(
items,
i => i.collection_position != null,
);
return (
<Box>
{pinned.length === 0 && unpinned.length === 0 ? (
<CollectionEmptyState />
) : (
<Box>
{pinned.length > 0 ? (
<Box mb={2}>
<Box mb={2}>
{pinned.length > 0 && (
<Box mb={2}>
<h4>{t`Pinned items`}</h4>
</Box>
)}
<h4>{t`Pinned items`}</h4>
</Box>
<PinDropTarget
pinIndex={1}
marginLeft={8}
marginRight={8}
noBorder
>
<Grid>
{pinned.map(item => (
<GridItem w={1 / 2}>
<Link
to={item.getUrl()}
className="hover-parent hover--visibility"
hover={{ color: normal.blue }}
>
<Card hoverable p={3}>
<Icon
name={item.getIcon()}
color={item.getColor()}
size={28}
mb={2}
/>
<Flex align="center">
<h3>{item.getName()}</h3>
{collection.can_write &&
item.setPinned && (
<Box
ml="auto"
className="hover-child"
onClick={ev => {
ev.preventDefault();
item.setPinned(false);
}}
>
<Icon name="pin" />
</Box>
)}
</Flex>
</Card>
</Link>
{pinned.map((item, index) => (
<GridItem w={1 / 2} className="relative">
<ItemDragSource item={item}>
<PinnedItem
key={`${item.type}:${item.id}`}
index={index}
item={item}
collection={collection}
/>
<PinPositionDropTarget pinIndex={index} left />
<PinPositionDropTarget
pinIndex={index + 1}
right
/>
</ItemDragSource>
</GridItem>
))}
{pinned.length % 2 === 1 ? (
<GridItem w={1 / 2} className="relative">
<PinPositionDropTarget pinIndex={pinned.length} />
</GridItem>
) : null}
</Grid>
</PinDropTarget>
</Box>
) : (
<PinDropTarget pinIndex={1} hideUntilDrag>
{({ hovered }) => (
<div
className={cx(
"p2 flex layout-centered",
hovered ? "text-brand" : "text-grey-2",
)}
>
<Icon name="pin" mr={1} />
{t`Drag something here to pin it to the top`}
</div>
)}
</PinDropTarget>
)}
<Flex align="center" mb={2}>
{pinned.length > 0 && (
<Box>
<h4>{t`Saved here`}</h4>
</Box>
<Flex align="center" mb={2}>
{pinned.length > 0 && (
<Box>
<h4>{t`Saved here`}</h4>
</Box>
)}
</Flex>
)}
</Flex>
{unpinned.length > 0 ? (
<PinDropTarget pinIndex={null} margin={8}>
<Card
mb={selected.length > 0 ? 5 : 2}
style={{ height: ROW_HEIGHT * other.length }}
style={{
position: "relative",
height: ROW_HEIGHT * unpinned.length,
}}
>
<VirtualizedList
items={other}
items={unpinned}
rowHeight={ROW_HEIGHT}
renderItem={({ item, index }) => (
<NormalItemContent
key={`${item.type}:${item.id}`}
item={item}
collection={collection}
reload={reload}
selection={selection}
onToggleSelected={onToggleSelected}
onMove={moveItems => this.setState({ moveItems })}
/>
<ItemDragSource item={item} selection={selection}>
<NormalItem
key={`${item.type}:${item.id}`}
item={item}
collection={collection}
selection={selection}
onToggleSelected={onToggleSelected}
onMove={moveItems => this.setState({ moveItems })}
/>
</ItemDragSource>
)}
/>
</Card>
</Box>
);
}}
</CollectionLoader>
</PinDropTarget>
) : (
<PinDropTarget pinIndex={null} hideUntilDrag margin={10}>
{({ hovered }) => (
<div
className={cx(
"m2 flex layout-centered",
hovered ? "text-brand" : "text-grey-2",
)}
>
{t`Drag here to un-pin`}
</div>
)}
</PinDropTarget>
)}
</Box>
)}
<BulkActionBar showing={selected.length > 0}>
<Flex align="center" w="100%">
{showCollectionList && (
......@@ -297,21 +347,22 @@ class DefaultLanding extends React.Component {
/>
</Modal>
)}
<ItemsDragLayer selected={selected} />
</Flex>
);
}
}
const NormalItemContent = ({
export const NormalItem = ({
item,
collection = {},
selection = new Set(),
onToggleSelected,
onMove,
reload,
}) => (
<Link to={item.getUrl()}>
<EntityItem
showSelect={selection.size > 0}
selectable
item={item}
type={item.type}
......@@ -343,6 +394,34 @@ const NormalItemContent = ({
</Link>
);
const PinnedItem = ({ item, index, collection }) => (
<Link
to={item.getUrl()}
className="hover-parent hover--visibility"
hover={{ color: normal.blue }}
>
<Card hoverable p={3}>
<Icon name={item.getIcon()} color={item.getColor()} size={28} mb={2} />
<Flex align="center">
<h3>{item.getName()}</h3>
{collection.can_write &&
item.setPinned && (
<Box
ml="auto"
className="hover-child"
onClick={ev => {
ev.preventDefault();
item.setPinned(false);
}}
>
<Icon name="pin" />
</Box>
)}
</Flex>
</Card>
</Link>
);
const BulkActionControls = ({ onArchive, onMove }) => (
<Box ml={1}>
<Button
......@@ -362,46 +441,44 @@ const SelectionControls = ({
onSelectNone,
}) =>
deselected.length === 0 ? (
<StackedCheckBox checked={true} onChange={onSelectNone} />
<StackedCheckBox checked onChange={onSelectNone} />
) : selected.length === 0 ? (
<StackedCheckBox onChange={onSelectAll} />
) : (
<StackedCheckBox checked={false} onChange={onSelectAll} />
);
// TODO - this should be a selector
const mapStateToProps = (state, props) => {
const collectionsById = Collections.selectors.expandedCollectionsById(
state,
props,
<StackedCheckBox checked indeterminate onChange={onSelectAll} />
);
return {
collectionId: props.params.collectionId,
collectionsById,
};
};
@connect(mapStateToProps)
@entityObjectLoader({
entityType: "collections",
entityId: (state, props) => props.params.collectionId,
})
class CollectionLanding extends React.Component {
render() {
const { collectionId, collectionsById } = this.props;
const currentCollection = collectionsById[collectionId];
const { object: currentCollection, params: { collectionId } } = this.props;
const isRoot = collectionId === "root";
// effective_ancestors doesn't include root collection so add it (unless this is the root collection, of course)
const ancestors =
!isRoot && currentCollection && currentCollection.effective_ancestors
? [ROOT_COLLECTION, ...currentCollection.effective_ancestors]
: [];
return (
<Box mx={4}>
<Box>
<Flex align="center">
<BrowserCrumbs
crumbs={
currentCollection && currentCollection.path
? [
...currentCollection.path.map(id => ({
title: collectionsById[id] && collectionsById[id].name,
to: Urls.collection(id),
})),
{ title: currentCollection.name },
]
: []
}
crumbs={[
...ancestors.map(({ id, name }) => ({
title: (
<CollectionDropTarget collection={{ id }} margin={8}>
{name}
</CollectionDropTarget>
),
to: Urls.collection(id),
})),
{ title: currentCollection.name },
]}
/>
<Flex ml="auto">
......@@ -428,7 +505,10 @@ class CollectionLanding extends React.Component {
</Flex>
</Box>
<Box>
<DefaultLanding collectionId={collectionId} />
<DefaultLanding
collection={currentCollection}
collectionId={collectionId}
/>
{
// Need to have this here so the child modals will show up
this.props.children
......
......@@ -28,7 +28,12 @@ export default class ErrorDetails extends React.Component {
style={{ fontFamily: "monospace" }}
className="QueryError2-detailBody bordered rounded bg-grey-0 text-bold p2 mt1"
>
{details}
{/* ensure we don't try to render anything except a string */}
{typeof details === "string"
? details
: typeof details.message === "string"
? details.message
: String(details)}
</div>
</div>
</div>
......
import React from "react";
import { Box, Flex } from "grid-styled";
export const GridItem = ({ children, w, px, py }) => (
<Box w={w} px={px} py={py}>
export const GridItem = ({ children, w, px, py, ...props }) => (
<Box w={w} px={px} py={py} {...props}>
{children}
</Box>
);
......
......@@ -13,7 +13,7 @@ const StackedCheckBox = props => (
zIndex: -1,
}}
>
<CheckBox {...props} />
<CheckBox {...props} noIcon />
</span>
<CheckBox {...props} />
</div>
......
......@@ -62,6 +62,7 @@ type Props = {
onBlur?: () => void,
updateOnInputChange: boolean,
updateOnInputBlur?: boolean,
// if provided, parseFreeformValue parses the input string into a value,
// or returns null to indicate an invalid value
parseFreeformValue: (value: string) => ?Value,
......@@ -346,6 +347,17 @@ export default class TokenField extends Component {
};
onInputBlur = () => {
if (this.props.updateOnInputBlur && this.props.parseFreeformValue) {
const input = findDOMNode(this.refs.input);
const value = this.props.parseFreeformValue(input.value);
if (
value != null &&
(this.props.multi || value !== this.props.value[0])
) {
this.addValue(value);
this.clearInputValue();
}
}
if (this.props.onBlur) {
this.props.onBlur();
}
......
......@@ -49,7 +49,8 @@ export default class Tooltip extends Component {
const isOpen =
this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
if (tooltip && isEnabled && isOpen) {
ReactDOM.render(
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
<TooltipPopover
isOpen={true}
target={this}
......
......@@ -6,7 +6,7 @@ import { Flex, Box } from "grid-styled";
import Icon from "metabase/components/Icon";
import Breadcrumbs from "metabase/components/Breadcrumbs";
import { getCollectionsById } from "metabase/entities/collections";
import { getExpandedCollectionsById } from "metabase/entities/collections";
const COLLECTION_ICON_COLOR = "#DCE1E4";
......@@ -55,7 +55,7 @@ export default class CollectionPicker extends React.Component {
const { value, onChange, collections, style, className } = this.props;
const { parentId } = this.state;
const collectionsById = getCollectionsById(collections);
const collectionsById = getExpandedCollectionsById(collections);
const collection = collectionsById[parentId];
const crumbs = this._getCrumbs(collection, collectionsById);
......
......@@ -42,7 +42,7 @@ class Overworld extends React.Component {
<CollectionItemsLoader collectionId="root">
{({ items }) => {
let pinnedDashboards = items.filter(
d => d.entity_type === "dashboards" && d.collection_position,
d => d.model === "dashboard" && d.collection_position != null,
);
if (!pinnedDashboards.length > 0) {
......
import { DropTarget } from "react-dnd";
import DropArea from "./DropArea";
import { MoveableDragTypes } from ".";
const CollectionDropTarget = DropTarget(
MoveableDragTypes,
{
drop(props, monitor, component) {
return { collection: props.collection };
},
canDrop(props, monitor) {
const { item } = monitor.getItem();
return item.model !== "collection" || item.id !== props.collection.id;
},
},
(connect, monitor) => ({
highlighted: monitor.canDrop(),
hovered: monitor.isOver() && monitor.canDrop(),
connectDropTarget: connect.dropTarget(),
}),
)(DropArea);
export default CollectionDropTarget;
import React from "react";
import cx from "classnames";
import { normal } from "metabase/lib/colors";
const DropTargetBackgroundAndBorder = ({
highlighted,
hovered,
noBorder = false,
margin = 0,
marginLeft = margin,
marginRight = margin,
marginTop = margin,
marginBottom = margin,
}) => (
<div
className={cx("absolute rounded", {
"pointer-events-none": !highlighted,
"bg-slate-almost-extra-light": highlighted,
})}
style={{
top: -marginTop,
left: -marginLeft,
bottom: -marginBottom,
right: -marginRight,
zIndex: -1,
boxSizing: "border-box",
border: "2px solid transparent",
borderColor: hovered & !noBorder ? normal.blue : "transparent",
}}
/>
);
export default class DropArea extends React.Component {
constructor(props) {
super(props);
this.state = {
show: this._shouldShow(props),
};
}
componentWillReceiveProps(nextProps) {
// need to delay showing/hiding due to Chrome bug where "dragend" is triggered
// immediately if the content shifts during "dragstart"
// https://github.com/react-dnd/react-dnd/issues/477
if (this._shouldShow(this.props) !== this._shouldShow(nextProps)) {
setTimeout(() => this.setState({ show: this._shouldShow(nextProps) }), 0);
}
}
_shouldShow(props) {
return !props.hideUntilDrag || props.highlighted;
}
render() {
const {
connectDropTarget,
children,
className,
style,
...props
} = this.props;
return this.state.show
? connectDropTarget(
<div className={cx("relative", className)} style={style}>
{typeof children === "function" ? children(props) : children}
<DropTargetBackgroundAndBorder {...props} />
</div>,
)
: null;
}
}
import React from "react";
import { DragSource } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";
import { dragTypeForItem } from ".";
@DragSource(
props => dragTypeForItem(props.item),
{
canDrag(props, monitor) {
// if items are selected only allow dragging selected items
if (
props.selection &&
props.selection.size > 0 &&
!props.selection.has(props.item)
) {
return false;
} else {
return true;
}
},
beginDrag(props, monitor, component) {
return { item: props.item };
},
async endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return;
}
const { item } = monitor.getItem();
const { collection, pinIndex } = monitor.getDropResult();
if (item) {
const items =
props.selection && props.selection.size > 0
? Array.from(props.selection)
: [item];
try {
if (collection !== undefined) {
await Promise.all(
items.map(i => i.setCollection && i.setCollection(collection)),
);
} else if (pinIndex !== undefined) {
await Promise.all(
items.map(i => i.setPinned && i.setPinned(pinIndex)),
);
}
} catch (e) {
alert("There was a problem moving these items: " + e);
}
}
},
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
}),
)
export default class ItemDragSource extends React.Component {
componentDidMount() {
// Use empty image as a drag preview so browsers don't draw it
// and we can draw whatever we want on the custom drag layer instead.
if (this.props.connectDragPreview) {
this.props.connectDragPreview(getEmptyImage(), {
// IE fallback: specify that we'd rather screenshot the node
// when it already knows it's being dragged so we can hide it with CSS.
captureDraggingState: true,
});
}
}
render() {
const { connectDragSource, children, ...props } = this.props;
return connectDragSource(
// must be a native DOM element or use innerRef which appears to be broken
// https://github.com/react-dnd/react-dnd/issues/1021
// https://github.com/jxnblk/styled-system/pull/188
<div>{typeof children === "function" ? children(props) : children}</div>,
);
}
}
import React from "react";
import { DragLayer } from "react-dnd";
import _ from "underscore";
import BodyComponent from "metabase/components/BodyComponent";
import { NormalItem } from "metabase/components/CollectionLanding";
// NOTE: our verison of react-hot-loader doesn't play nice with react-dnd's DragLayer, so we exclude files named `*DragLayer.jsx` in webpack.config.js
@DragLayer((monitor, props) => ({
item: monitor.getItem(),
// itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
isDragging: monitor.isDragging(),
}))
@BodyComponent
export default class ItemsDragLayer extends React.Component {
render() {
const { isDragging, currentOffset, selected, item } = this.props;
if (!isDragging || !currentOffset) {
return null;
}
const items = selected.length > 0 ? selected : [item.item];
return (
<div
style={{
position: "absolute",
top: 0,
left: 0,
transform: `translate(${currentOffset.x}px, ${currentOffset.y}px)`,
pointerEvents: "none",
}}
>
<DraggedItems items={items} draggedItem={item.item} />
</div>
);
}
}
class DraggedItems extends React.Component {
shouldComponentUpdate(nextProps) {
// necessary for decent drag performance
return (
nextProps.items.length !== this.props.items.length ||
nextProps.draggedItem !== this.props.draggedItem
);
}
render() {
const { items, draggedItem } = this.props;
const index = _.findIndex(items, draggedItem);
return (
<div
style={{
position: "absolute",
transform: index > 0 ? `translate(0px, ${-index * 72}px)` : null,
}}
>
{items.map(item => <NormalItem item={item} />)}
</div>
);
}
}
import { DropTarget } from "react-dnd";
import DropArea from "./DropArea";
import { PinnableDragTypes } from ".";
const PinDropTarget = DropTarget(
PinnableDragTypes,
{
drop(props, monitor, component) {
return { pinIndex: props.pinIndex };
},
canDrop(props, monitor) {
const { item } = monitor.getItem();
return props.pinIndex != item.collection_position;
},
},
(connect, monitor) => ({
highlighted: monitor.canDrop(),
hovered: monitor.isOver() && monitor.canDrop(),
connectDropTarget: connect.dropTarget(),
}),
)(DropArea);
export default PinDropTarget;
import React from "react";
import { DropTarget } from "react-dnd";
import cx from "classnames";
import { PinnableDragTypes } from "./index";
const PIN_DROP_TARGET_INDICATOR_WIDTH = 3;
@DropTarget(
PinnableDragTypes,
{
drop(props, monitor, component) {
return { pinIndex: props.pinIndex };
},
},
(connect, monitor) => ({
highlighted: monitor.canDrop(),
hovered: monitor.isOver() && monitor.canDrop(),
connectDropTarget: connect.dropTarget(),
}),
)
export default class PinPositionDropTarget extends React.Component {
render() {
const {
left,
right,
connectDropTarget,
hovered,
highlighted,
offset = 0,
} = this.props;
return connectDropTarget(
<div
className={cx("absolute top bottom", {
"pointer-events-none": !highlighted,
})}
style={{
width: left | right ? "50%" : undefined,
left: !right ? 0 : undefined,
right: !left ? 0 : undefined,
}}
>
<div
className={cx("absolute", { "bg-brand": hovered })}
style={{
top: 10,
bottom: 10,
width: PIN_DROP_TARGET_INDICATOR_WIDTH,
left: !right
? -PIN_DROP_TARGET_INDICATOR_WIDTH / 2 - offset
: undefined,
right: right
? -PIN_DROP_TARGET_INDICATOR_WIDTH / 2 - offset
: undefined,
}}
/>
</div>,
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment