diff --git a/frontend/src/metabase/components/CollectionLanding.jsx b/frontend/src/metabase/components/CollectionLanding.jsx index 426f2b4c05368d3d6bc7165ea78c99ea39adce41..20af751874a23cc64689dff9580e2250062398f7 100644 --- a/frontend/src/metabase/components/CollectionLanding.jsx +++ b/frontend/src/metabase/components/CollectionLanding.jsx @@ -3,7 +3,7 @@ import { Box, Flex, Subhead, Truncate } from "rebass"; import { t } from "c-3po"; import { connect } from "react-redux"; import { withRouter } from "react-router"; -import listSelect from "metabase/hoc/ListSelect"; +import listSelect from "metabase/hoc/ListSelect" import Question from "metabase/entities/questions"; import Dashboard from "metabase/entities/dashboards"; @@ -12,6 +12,7 @@ import * as Urls from "metabase/lib/urls"; import { normal } from "metabase/lib/colors"; import Card from "metabase/components/Card"; +import CheckBox from "metabase/components/CheckBox"; import EntityItem from "metabase/components/EntityItem"; import { Grid, GridItem } from "metabase/components/Grid"; import Icon from "metabase/components/Icon"; @@ -83,77 +84,16 @@ const CollectionList = () => { ); }; -@withRouter @listSelect() -@connect(() => ({}), mapDispatchToProps) +@connect(null, mapDispatchToProps) class DefaultLanding extends React.Component { state = { reload: false, }; - _getItemProps(item) { - switch (item.type) { - case "card": - return { - url: Urls.question(item.id), - iconName: "beaker", - iconColor: "#93B3C9", - }; - case "dashboard": - return { - url: Urls.dashboard(item.id), - iconName: "dashboard", - iconColor: normal.blue, - }; - case "pulse": - return { - url: Urls.pulseEdit(item.id), - iconName: "pulse", - iconColor: normal.yellow, - }; - } - } - _reload() { - this.setState({ reload: true }); - setTimeout(() => this.setState({ relaod: false }), 2000); - } - async _pinItem({ id, type, collection_position }) { - const { updateQuestion, updateDashboard } = this.props; - switch (type) { - case "card": - // hack in 1 as the collection position just to be able to get "pins" - await updateQuestion({ id, collection_position: 1 }); - break; - case "dashboard": - await updateDashboard({ id, collection_position: 1 }); - break; - } - this._reload(); - } - - async _unPinItem({ id, type, collection_position }) { - const { updateQuestion, updateDashboard } = this.props; - switch (type) { - case "card": - await updateQuestion({ id, collection_position: null }); - break; - case "dashboard": - await updateDashboard({ id, collection_position: null }); - break; - } - this._reload(); - } - render() { - const { - collectionId, - location, - selected, - onToggleSelected, - selection, - } = this.props; + const { collectionId, onToggleSelected, selection } = this.props; - console.log(this.props); // Show the const showCollectionList = collectionId === "root"; @@ -169,35 +109,16 @@ class DefaultLanding extends React.Component { )} <Box w={2 / 3}> <Box> - <CollectionItemsLoader reload collectionId={collectionId || "root"}> - {({ collection, allItems, pulses, cards, dashboards, empty }) => { - let items = allItems; - - if (!items.length) { + <CollectionItemsLoader + reload + wrapped + collectionId={collectionId || "root"} + > + {({ collection, items }) => { + if (items.length === 0) { return <CollectionEmptyState />; } - // Hack in filtering - if (location.query.show) { - switch (location.query.show) { - case "dashboards": - items = dashboards.map(d => ({ - ...d, - type: "dashboard", - })); - break; - case "pulses": - items = pulses.map(p => ({ ...p, type: "pulse" })); - break; - case "questions": - items = cards.map(c => ({ ...c, type: "card" })); - break; - default: - items = allItems; - break; - } - } - const pinned = items.filter(i => i.collection_position); const other = items.filter(i => !i.collection_position); @@ -210,47 +131,40 @@ class DefaultLanding extends React.Component { </Box> )} <Grid> - {pinned.map(item => { - // TODO - move this over to use item fns like getUrl() - const { - url, - iconName, - iconColor, - } = this._getItemProps(item); - return ( - <GridItem w={1 / 2}> - <Link - to={url} - className="hover-parent hover--visibility" - hover={{ color: normal.blue }} - > - <Card hoverable p={3}> - <Icon - name={iconName} - color={iconColor} - size={28} - mb={2} - /> - <Flex align="center"> - <h3>{item.name}</h3> - {collection.can_write && ( + {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.unpin && ( <Box ml="auto" className="hover-child" onClick={ev => { ev.preventDefault(); - this._unPinItem(item); + item.unpin(); }} > <Icon name="pin" /> </Box> )} - </Flex> - </Card> - </Link> - </GridItem> - ); - })} + </Flex> + </Card> + </Link> + </GridItem> + ))} </Grid> </Box> <Flex align="center" mb={2}> @@ -262,32 +176,32 @@ class DefaultLanding extends React.Component { </Flex> <Card> {other.map(item => { - const { url, iconName, iconColor } = this._getItemProps( - item, - ); return ( - <Box> - <Link to={url}> + <Box key={item.type + item.id}> + <Link to={item.getUrl()}> <EntityItem + selectable item={item} - name={item.name} - iconName={iconName} - iconColor={iconColor} + type={item.type} + name={item.getName()} + iconName={item.getIcon()} + iconColor={item.getColor()} + onFavorite={item.setFavorited} + isFavorite={item.getFavorited && item.getFavorited()} onPin={ - collection.can_write - ? this._pinItem.bind(this) + collection.can_write && item.pin + ? () => item.pin() : null } selected={selection.has(item)} - onToggleSelected={ev => { - ev.preventDefault(); - onToggleSelected(item); + onToggleSelected={(ev) => { + ev.preventDefault() + onToggleSelected(item) }} /> </Link> </Box> - ); - })} + )})} </Card> </Box> ); diff --git a/frontend/src/metabase/components/EntityItem.jsx b/frontend/src/metabase/components/EntityItem.jsx index b78d1ed2ba0a6451b74864e2abee5b7f814b15f3..185d5074005c9b17b479e0c0b7b63c238ad18f65 100644 --- a/frontend/src/metabase/components/EntityItem.jsx +++ b/frontend/src/metabase/components/EntityItem.jsx @@ -1,6 +1,7 @@ import React from "react"; import { t } from "c-3po"; import EntityMenu from "metabase/components/EntityMenu"; +import { Motion, spring } from "react-motion" import { Flex, Box, Truncate } from "rebass"; @@ -31,9 +32,12 @@ const EntityItem = ({ iconName, iconColor, item, + isFavorite, onPin, + onFavorite, selected, onToggleSelected, + selectable }) => { return ( <EntityItemWrapper py={2} px={2} className="hover-parent hover--visibility"> @@ -42,14 +46,20 @@ const EntityItem = ({ mr={1} align="center" justify="center" - className="hover-parent hover--visibility" > - <CheckBox - checked={selected} - onChange={onToggleSelected} - className="hover-child" + { selectable ? ( + <Swapper + defaultElement={<Icon name={iconName} color={iconColor} />} + swappedElement={ + <CheckBox + checked={selected} + onChange={(ev) => onToggleSelected(ev)} + /> + } /> + ) : ( <Icon name={iconName} color={iconColor} /> + )} </IconWrapper> <h3> <Truncate>{name}</Truncate> @@ -61,11 +71,13 @@ const EntityItem = ({ className="hover-child" onClick={e => e.preventDefault()} > + { onFavorite && ( <Icon - name="staroutline" + name={isFavorite ? "star" : "staroutline"} mr={1} - onClick={() => item.setFavorited(item)} + onClick={() => onFavorite(item)} /> + )} <EntityMenu triggerIcon="ellipsis" items={[ @@ -91,4 +103,73 @@ const EntityItem = ({ ); }; +class Swapper extends React.Component { + props: { + defaultElement: React$Element, + swappedElement: React$Element + } + + state = { + hovered: false + } + + _onMouseEnter () { + this.setState({ hovered: true }) + } + + _onMouseLeave () { + this.setState({ hovered: false }) + } + + render () { + const { defaultElement, swappedElement } = this.props + const { hovered } = this.state + + return ( + <span + onMouseEnter={() => this._onMouseEnter()} + onMouseLeave={() => this._onMouseLeave()} + className="block relative" + > + <Motion + defaultStyle={{ + scale: 1 + }} + style={{ + scale: hovered ? spring(0): spring(1) + }} + > + {({ scale }) => { + return ( + <span className="" style={{ display: 'block', transform: `scale(${scale})` }}> + { defaultElement } + </span> + ) + }} + </Motion> + <Motion + defaultStyle={{ + scale: 0 + }} + style={{ + scale: hovered ? spring(1): spring(0) + }} + > + {({ scale }) => { + return ( + <span className="absolute top left bottom right" style={{ display: 'block', transform: `scale(${scale})` }}> + { swappedElement } + </span> + ) + }} + </Motion> + </span> + ) + } +} + +EntityItem.defaultProps = { + selectable: false +} + export default EntityItem; diff --git a/frontend/src/metabase/containers/CollectionItemsLoader.jsx b/frontend/src/metabase/containers/CollectionItemsLoader.jsx index 02ff760143f786803e2ee0895aa4e07f7c027fa7..e22ae4976abdcd7dab1bd8a50f0e5dabf786b30e 100644 --- a/frontend/src/metabase/containers/CollectionItemsLoader.jsx +++ b/frontend/src/metabase/containers/CollectionItemsLoader.jsx @@ -18,6 +18,7 @@ const CollectionItemsLoader = ({ collectionId, children, ...props }: Props) => ( {...props} entityType="search" entityQuery={{ collectionId }} + wrapped children={({ list }) => object && list && diff --git a/frontend/src/metabase/entities/dashboards.js b/frontend/src/metabase/entities/dashboards.js index e4d47f9a928330dfd8e754779d6e2315aa3276b1..b15e5274243585c38f180cadb6b9095d80fdba3e 100644 --- a/frontend/src/metabase/entities/dashboards.js +++ b/frontend/src/metabase/entities/dashboards.js @@ -18,10 +18,14 @@ const Dashboards = createEntity({ }), pin: ({ id }) => Dashboards.actions.update({ id, collection_position: 1 }), unpin: ({ id }) => - Dashboards.actions.update({ id, collection_position: null }), + Dashboards.actions.update({ id, collection_position: null }), + setFavorited: ({id}, favorited) => + Dashboards.actions.update({ id, favorited }), + }, objectSelectors: { + getFavorited: dashboard => dashboard && dashboard.favorited, getName: dashboard => dashboard && dashboard.name, getUrl: dashboard => dashboard && Urls.dashboard(dashboard.id), getIcon: dashboard => "dashboard", diff --git a/frontend/src/metabase/entities/questions.js b/frontend/src/metabase/entities/questions.js index 06098c265cf18afd8b9edd76af0189a5048ad08b..7ac342672099469f7bfe6594c40c3a4825a90a29 100644 --- a/frontend/src/metabase/entities/questions.js +++ b/frontend/src/metabase/entities/questions.js @@ -21,13 +21,14 @@ const Questions = createEntity({ unpin: ({ id }) => Questions.actions.update({ id, collection_position: null }), setFavorited: ({ id }, favorited) => - Questions.actions.updated({ + Questions.actions.update({ id, favorited, }), }, objectSelectors: { + getFavorited: question => question && question.favorited, getName: question => question && question.name, getUrl: question => question && Urls.question(question.id), getColor: () => "#93B3C9",