From 2baf17681bf74f1625406e2a0273416ccd095c34 Mon Sep 17 00:00:00 2001 From: Tom Robinson <tlrobinson@gmail.com> Date: Mon, 11 Apr 2016 12:15:56 -0700 Subject: [PATCH] Rework questions redux/selectors --- frontend/src/Routes.jsx | 1 - frontend/src/lib/redux.js | 5 +- .../questions/{components => }/Questions.css | 0 .../src/questions/components/ActionHeader.css | 25 ++- .../src/questions/components/ActionHeader.jsx | 33 ++- frontend/src/questions/components/Item.jsx | 51 +++++ .../src/questions/components/LabelPopover.jsx | 8 + .../{QuestionsList.css => List.css} | 8 +- frontend/src/questions/components/List.jsx | 15 ++ .../src/questions/components/QuestionItem.jsx | 46 ----- .../questions/components/QuestionsList.jsx | 28 --- .../src/questions/components/SearchHeader.css | 2 +- .../{QuestionsSidebar.css => Sidebar.css} | 0 .../{QuestionsSidebar.jsx => Sidebar.jsx} | 7 +- .../questions/components/SidebarLayout.jsx | 4 +- .../questions/containers/EntityBrowser.jsx | 20 +- .../src/questions/containers/EntityItem.jsx | 40 +++- .../src/questions/containers/EntityList.jsx | 48 +++-- frontend/src/questions/duck.js | 69 +++++-- frontend/src/questions/selectors.js | 192 ++++++++++-------- package.json | 2 + 21 files changed, 378 insertions(+), 226 deletions(-) rename frontend/src/questions/{components => }/Questions.css (100%) create mode 100644 frontend/src/questions/components/Item.jsx create mode 100644 frontend/src/questions/components/LabelPopover.jsx rename frontend/src/questions/components/{QuestionsList.css => List.css} (92%) create mode 100644 frontend/src/questions/components/List.jsx delete mode 100644 frontend/src/questions/components/QuestionItem.jsx delete mode 100644 frontend/src/questions/components/QuestionsList.jsx rename frontend/src/questions/components/{QuestionsSidebar.css => Sidebar.css} (100%) rename frontend/src/questions/components/{QuestionsSidebar.jsx => Sidebar.jsx} (92%) diff --git a/frontend/src/Routes.jsx b/frontend/src/Routes.jsx index d808d539258..94377d0d160 100644 --- a/frontend/src/Routes.jsx +++ b/frontend/src/Routes.jsx @@ -1,5 +1,4 @@ import React, { Component, PropTypes } from "react"; -import { Link } from "react-router"; import { Route } from 'react-router'; import { ReduxRouter } from 'redux-router'; diff --git a/frontend/src/lib/redux.js b/frontend/src/lib/redux.js index 3346e82ad22..3d101e2159d 100644 --- a/frontend/src/lib/redux.js +++ b/frontend/src/lib/redux.js @@ -4,6 +4,7 @@ import _ from "underscore"; import { createStore as originalCreateStore, applyMiddleware, compose } from "redux"; import promise from 'redux-promise'; import thunk from "redux-thunk"; +import createLogger from "redux-logger"; import { createHistory } from 'history'; import { reduxReactRouter } from 'redux-router'; @@ -12,9 +13,11 @@ import { reduxReactRouter } from 'redux-router'; export { combineReducers } from "redux"; export { handleActions, createAction } from "redux-actions"; +const logger = createLogger(); + // common createStore with middleware applied export const createStore = compose( - applyMiddleware(thunk, promise), + applyMiddleware(thunk, promise, logger), reduxReactRouter({ createHistory }) )(originalCreateStore); diff --git a/frontend/src/questions/components/Questions.css b/frontend/src/questions/Questions.css similarity index 100% rename from frontend/src/questions/components/Questions.css rename to frontend/src/questions/Questions.css diff --git a/frontend/src/questions/components/ActionHeader.css b/frontend/src/questions/components/ActionHeader.css index 7ec4dab4370..ba56527a00a 100644 --- a/frontend/src/questions/components/ActionHeader.css +++ b/frontend/src/questions/components/ActionHeader.css @@ -1,4 +1,4 @@ -@import './Questions.css'; +@import '../Questions.css'; :local(.actionHeader) { composes: flex align-center from "style/flex"; @@ -13,6 +13,27 @@ /* ALL CHECKBOX */ :local(.allCheckbox) { - composes: icon from "./QuestionsList.css"; + composes: icon from "./List.css"; visibility: visible !important; } + +:local(.labelButton) { + composes: px1 from "style/spacing"; + composes: cursor-pointer from "style/cursor"; +} + +:local(.archiveButton) { + composes: px1 from "style/spacing"; + composes: cursor-pointer from "style/cursor"; +} + + +:local(.labelButton) .Icon, +:local(.archiveButton) .Icon { + padding-left: 0.25em; + padding-right: 0.25em; +} + +:local(.labelButton) .Icon-chevrondown { + color: #DEEAF1; +} diff --git a/frontend/src/questions/components/ActionHeader.jsx b/frontend/src/questions/components/ActionHeader.jsx index d8fc64033c7..a74f2aab31f 100644 --- a/frontend/src/questions/components/ActionHeader.jsx +++ b/frontend/src/questions/components/ActionHeader.jsx @@ -2,17 +2,42 @@ import React, { Component, PropTypes } from "react"; import S from "./ActionHeader.css"; import StackedCheckBox from "metabase/components/StackedCheckBox.jsx"; +import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx"; +import Icon from "metabase/components/Icon.jsx"; -const ActionHeader = ({ selectedCount, allSelected }) => +import LabelPopover from "./LabelPopover.jsx"; + +const ActionHeader = ({ selectedCount, allSelected, setAllSelected, archiveSelected }) => <div className={S.actionHeader}> - <StackedCheckBox checked={allSelected} className={S.allCheckbox} size={20} padding={3} borderColor="currentColor" invertChecked /> + <StackedCheckBox + checked={allSelected} + onChange={(e) => setAllSelected(e.target.checked)} + className={S.allCheckbox} + size={20} padding={3} borderColor="currentColor" + invertChecked + /> <span className={S.selectedCount}> {selectedCount} selected </span> <span className="flex-align-right"> - <span>Labels</span> - <span>Archive</span> + <PopoverWithTrigger + triggerElement={ + <span className={S.labelButton}> + <Icon name="grid" /> + Labels + <Icon name="chevrondown" width={12} height={12} /> + </span> + } + > + <LabelPopover /> + </PopoverWithTrigger> + <span className={S.archiveButton} onClick={archiveSelected}> + <Icon name="grid" /> + Archive + </span> </span> </div> + + export default ActionHeader; diff --git a/frontend/src/questions/components/Item.jsx b/frontend/src/questions/components/Item.jsx new file mode 100644 index 00000000000..1fe0fca90cb --- /dev/null +++ b/frontend/src/questions/components/Item.jsx @@ -0,0 +1,51 @@ +import React, { Component, PropTypes } from "react"; +import S from "./List.css"; + +import Labels from "./Labels.jsx"; + +import Icon from "metabase/components/Icon.jsx"; +import CheckBox from "metabase/components/CheckBox.jsx"; + +import cx from "classnames"; +import pure from "recompose/pure"; + +const Item = ({ id, name, created, by, selected, favorite, icon, labels, setItemSelected }) => + <div className={cx(S.item, { [S.selected]: selected, [S.favorite]: favorite })}> + <div className={S.leftIcons}> + { icon && <Icon className={S.chartIcon} name={icon} width={32} height={32} /> } + <CheckBox + checked={selected} + onChange={(e) => setItemSelected({ [id]: e.target.checked })} + className={S.itemCheckbox} + size={20} + padding={3} + borderColor="currentColor" + invertChecked + /> + </div> + <ItemBody name={name} labels={labels} created={created} by={by} /> + <div className={S.rightIcons}> + <Icon className={S.favoriteIcon} name="star" width={20} height={20} /> + </div> + <div className={S.extraIcons}> + <Icon className={S.archiveIcon} name="grid" width={20} height={20} /> + </div> + </div> + +const ItemBody = pure(({ name, labels, created, by }) => + <div className={S.itemBody}> + <div className={S.itemTitle}> + <span className={S.itemName}>{name}</span> + <Labels labels={labels} /> + <Icon className={S.tagIcon} name="grid" /> + </div> + <div className={S.itemSubtitle}> + {"Created "} + <span className={S.itemSubtitleBold}>{created}</span> + {" by "} + <span className={S.itemSubtitleBold}>{by}</span> + </div> + </div> +) + +export default pure(Item); diff --git a/frontend/src/questions/components/LabelPopover.jsx b/frontend/src/questions/components/LabelPopover.jsx new file mode 100644 index 00000000000..ca4fff220e2 --- /dev/null +++ b/frontend/src/questions/components/LabelPopover.jsx @@ -0,0 +1,8 @@ +import React, { Component, PropTypes } from "react"; + +const LabelPopover = ({ count }) => + <div> + Apply labels to {count} questions + </div> + +export default LabelPopover; diff --git a/frontend/src/questions/components/QuestionsList.css b/frontend/src/questions/components/List.css similarity index 92% rename from frontend/src/questions/components/QuestionsList.css rename to frontend/src/questions/components/List.css index b44192530c7..993843dd7b2 100644 --- a/frontend/src/questions/components/QuestionsList.css +++ b/frontend/src/questions/components/List.css @@ -1,4 +1,4 @@ -@import './Questions.css'; +@import '../Questions.css'; :local(.list) { padding-left: 75px; @@ -82,10 +82,10 @@ visibility: visible !important; } :local(.item):hover :local(.itemCheckbox), -:local(.item.checked) :local(.itemCheckbox) { +:local(.item.selected) :local(.itemCheckbox) { display: inline; } -:local(.item.checked) :local(.itemCheckbox) { +:local(.item.selected) :local(.itemCheckbox) { color: var(--blue-color); } @@ -97,7 +97,7 @@ left: -6px; } :local(.item):hover :local(.chartIcon), -:local(.item.checked) :local(.chartIcon) { +:local(.item.selected) :local(.chartIcon) { display: none; } diff --git a/frontend/src/questions/components/List.jsx b/frontend/src/questions/components/List.jsx new file mode 100644 index 00000000000..5206ab23b3c --- /dev/null +++ b/frontend/src/questions/components/List.jsx @@ -0,0 +1,15 @@ +import React, { Component, PropTypes } from "react"; +import S from "./List.css"; + +import { pure } from "recompose"; + +import EntityItem from "../containers/EntityItem.jsx"; + +const List = ({ entityType, entityIds, setItemSelected }) => + <ul> + { entityIds.map(entityId => + <EntityItem key={entityId} entityType={entityType} entityId={entityId} setItemSelected={setItemSelected} /> + )} + </ul> + +export default pure(List); diff --git a/frontend/src/questions/components/QuestionItem.jsx b/frontend/src/questions/components/QuestionItem.jsx deleted file mode 100644 index b9a006115eb..00000000000 --- a/frontend/src/questions/components/QuestionItem.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { Component, PropTypes } from "react"; -import S from "./QuestionsList.css"; - -import Labels from "./Labels.jsx"; - -import Icon from "metabase/components/Icon.jsx"; -import CheckBox from "metabase/components/CheckBox.jsx"; - -import cx from "classnames"; -import pure from "recompose/pure"; - -const QuestionItem = ({ id, name, created, by, checked, favorite, iconName, labels, setItemChecked }) => - <li className={cx(S.item, { [S.checked]: checked, [S.favorite]: favorite })}> - <div className={S.leftIcons}> - { iconName && <Icon className={S.chartIcon} name={iconName} width={32} height={32} /> } - <CheckBox - checked={checked} - onChange={(e) => setItemChecked({ [id]: e.target.checked })} - className={S.itemCheckbox} - size={20} - padding={3} - borderColor="currentColor" - invertChecked - /> - </div> - <div className={S.itemBody}> - <div className={S.itemTitle}> - <span className={S.itemName}>{name}</span> - <Labels labels={labels} /> - <Icon className={S.tagIcon} name="grid" /> - </div> - <div className={S.itemSubtitle}> - {"Created "} - <span className={S.itemSubtitleBold}>{created}</span> - {" by "} - <span className={S.itemSubtitleBold}>{by}</span></div> - </div> - <div className={S.rightIcons}> - <Icon className={S.favoriteIcon} name="star" width={20} height={20} /> - </div> - <div className={S.extraIcons}> - <Icon className={S.archiveIcon} name="grid" width={20} height={20} /> - </div> - </li> - -export default pure(QuestionItem); diff --git a/frontend/src/questions/components/QuestionsList.jsx b/frontend/src/questions/components/QuestionsList.jsx deleted file mode 100644 index 46bece39209..00000000000 --- a/frontend/src/questions/components/QuestionsList.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { Component, PropTypes } from "react"; -import S from "./QuestionsList.css"; - -import SearchHeader from "./SearchHeader.jsx"; -import ActionHeader from "./ActionHeader.jsx"; -import QuestionItem from "./QuestionItem.jsx"; - -const QuestionsList = ({ style, questions, name, selectedCount, allSelected, searchText, setSearchText, setItemChecked }) => - <div style={style} className={S.list}> - <div className={S.header}> - {name} - </div> - { selectedCount > 0 ? - <ActionHeader - selectedCount={selectedCount} - allSelected={allSelected} - /> - : - <SearchHeader searchText={searchText} setSearchText={setSearchText} /> - } - <ul> - { questions.map(question => - <QuestionItem key={question.id} {...question} setItemChecked={setItemChecked} /> - )} - </ul> - </div> - -export default QuestionsList; diff --git a/frontend/src/questions/components/SearchHeader.css b/frontend/src/questions/components/SearchHeader.css index 78499709f70..2f389c5b92d 100644 --- a/frontend/src/questions/components/SearchHeader.css +++ b/frontend/src/questions/components/SearchHeader.css @@ -1,4 +1,4 @@ -@import './Questions.css'; +@import '../Questions.css'; :local(.searchHeader) { composes: flex align-center from "style/flex"; diff --git a/frontend/src/questions/components/QuestionsSidebar.css b/frontend/src/questions/components/Sidebar.css similarity index 100% rename from frontend/src/questions/components/QuestionsSidebar.css rename to frontend/src/questions/components/Sidebar.css diff --git a/frontend/src/questions/components/QuestionsSidebar.jsx b/frontend/src/questions/components/Sidebar.jsx similarity index 92% rename from frontend/src/questions/components/QuestionsSidebar.jsx rename to frontend/src/questions/components/Sidebar.jsx index f6b70d3d3f9..7079fb9ea95 100644 --- a/frontend/src/questions/components/QuestionsSidebar.jsx +++ b/frontend/src/questions/components/Sidebar.jsx @@ -1,12 +1,13 @@ import React, { Component, PropTypes } from "react"; import { Link } from "react-router"; -import S from "./QuestionsSidebar.css"; +import S from "./Sidebar.css"; import Icon from "metabase/components/Icon.jsx"; +import { pure } from "recompose"; import cx from "classnames"; -const QuestionsSidebar = ({ sections, topics, labels }) => +const Sidebar = ({ sections, topics, labels }) => <div className={S.sidebar}> <ul> {sections.map(section => @@ -46,4 +47,4 @@ const QuestionSidebarIcon = ({ icon }) => : <Icon className={S.icon} name={icon} /> -export default QuestionsSidebar; +export default pure(Sidebar); diff --git a/frontend/src/questions/components/SidebarLayout.jsx b/frontend/src/questions/components/SidebarLayout.jsx index 29aa02c936f..f93a4f5e5cc 100644 --- a/frontend/src/questions/components/SidebarLayout.jsx +++ b/frontend/src/questions/components/SidebarLayout.jsx @@ -1,6 +1,6 @@ import React, { Component, PropTypes } from "react"; -const SidebarLayout = ({ className, style, sidebar, children }) => { console.log("children", children); return ( +const SidebarLayout = ({ className, style, sidebar, children }) => <div className={className} style={{ ...style, display: "flex", flexDirection: "row" }}> { React.cloneElement( sidebar, @@ -13,5 +13,5 @@ const SidebarLayout = ({ className, style, sidebar, children }) => { console.log React.Children.only(children).props.children )} </div> -)} + export default SidebarLayout; diff --git a/frontend/src/questions/containers/EntityBrowser.jsx b/frontend/src/questions/containers/EntityBrowser.jsx index d655cd39497..0db42dfe0fd 100644 --- a/frontend/src/questions/containers/EntityBrowser.jsx +++ b/frontend/src/questions/containers/EntityBrowser.jsx @@ -1,24 +1,20 @@ import React, { Component, PropTypes } from "react"; +import ReactDOM from "react-dom"; import { connect } from "react-redux"; -import QuestionsSidebar from "../components/QuestionsSidebar.jsx"; +import Sidebar from "../components/Sidebar.jsx"; import SidebarLayout from "../components/SidebarLayout.jsx"; import cx from "classnames"; import * as questionsActions from "../duck"; -import { getSections, getTopics, getLabels, getSearchText, getQuestionItemsFilteredBySearchText } from "../selectors"; +import { getSections, getTopics, getLabels } from "../selectors"; const mapStateToProps = (state, props) => { return { sections: getSections(state), topics: getTopics(state), - labels: getLabels(state), - questions: getQuestionItemsFilteredBySearchText(state), - searchText: getSearchText(state), - - name: "foo", - selectedCount: 0 + labels: getLabels(state) } } @@ -34,22 +30,20 @@ export default class EntityBrowser extends Component { }; componentWillMount() { - this.props.selectQuestionSection(this.props.params.section, this.props.params.slug); + this.props.selectSection(this.props.params.section, this.props.params.slug); } componentWillReceiveProps(newProps) { - console.log(newProps.params) if (this.props.params.section !== newProps.params.section || this.props.params.slug !== newProps.params.slug) { - this.props.selectQuestionSection(newProps.params.section, newProps.params.slug); + this.props.selectSection(newProps.params.section, newProps.params.slug); } } render() { - console.log(this.props); return ( <SidebarLayout className={cx("spread")} - sidebar={<QuestionsSidebar {...this.props} children={undefined}/>} + sidebar={<Sidebar {...this.props} children={undefined}/>} > {this.props.children} </SidebarLayout> diff --git a/frontend/src/questions/containers/EntityItem.jsx b/frontend/src/questions/containers/EntityItem.jsx index e69488687a5..37a0374ab20 100644 --- a/frontend/src/questions/containers/EntityItem.jsx +++ b/frontend/src/questions/containers/EntityItem.jsx @@ -1,5 +1,28 @@ import React, { Component, PropTypes } from "react"; +import { connect } from "react-redux"; +import Item from "../components/Item.jsx"; + +import * as questionsActions from "../duck"; +import { makeGetItem } from "../selectors"; + +// const mapStateToProps = (state, props) => { +// return { +// item: getItem(state, props) +// } +// } + +const makeMapStateToProps = () => { + const getItem = makeGetItem() + const mapStateToProps = (state, props) => { + return { + item: getItem(state, props) + }; + }; + return mapStateToProps; +} + +@connect(makeMapStateToProps, questionsActions) export default class EntityItem extends Component { constructor(props, context) { super(props, context); @@ -10,8 +33,21 @@ export default class EntityItem extends Component { static defaultProps = {}; render() { + let { item, setItemSelected } = this.props; return ( - <div>hello</div> - ); + <li style={{ display: item.visible ? undefined : "none" }}> + <Item + id={item.id} + name={item.name} + created={item.created} + by={item.by} + favorite={item.favorite} + icon={item.icon} + selected={item.selected} + labels={item.labels} + setItemSelected={setItemSelected} + /> + </li> + ) } } diff --git a/frontend/src/questions/containers/EntityList.jsx b/frontend/src/questions/containers/EntityList.jsx index 8dc71867ae3..cac2cd19206 100644 --- a/frontend/src/questions/containers/EntityList.jsx +++ b/frontend/src/questions/containers/EntityList.jsx @@ -1,40 +1,48 @@ import React, { Component, PropTypes } from "react"; import { connect } from "react-redux"; -import QuestionsList from "../components/QuestionsList.jsx"; +import S from "../components/List.css"; + +import List from "../components/List.jsx"; +import SearchHeader from "../components/SearchHeader.jsx"; +import ActionHeader from "../components/ActionHeader.jsx"; import * as questionsActions from "../duck"; -import { getSections, getTopics, getLabels, getSearchText, getChecked, getQuestionItemsFilteredBySearchText } from "../selectors"; +import { getSearchText, getEntityType, getEntityIds, getSectionName, getSelectedCount, getVisibleCount } from "../selectors"; const mapStateToProps = (state, props) => { return { - sections: getSections(state), - topics: getTopics(state), - labels: getLabels(state), - questions: getQuestionItemsFilteredBySearchText(state), - searchText: getSearchText(state), + entityType: getEntityType(state), + entityIds: getEntityIds(state), - name: "foo", - selectedCount: 0, + searchText: getSearchText(state), - checked: getChecked(state) + name: getSectionName(state), + selectedCount: getSelectedCount(state), + visibleCount: getVisibleCount(state) } } @connect(mapStateToProps, questionsActions) export default class EntityList extends Component { - constructor(props, context) { - super(props, context); - this.state = {}; - } - - static propTypes = {}; - static defaultProps = {}; - render() { - console.log("PROPS", this.props) + const { style, name, selectedCount, visibleCount, searchText, setSearchText, entityType, entityIds, setItemSelected, setAllSelected } = this.props; return ( - <QuestionsList {...this.props} /> + <div style={style} className={S.list}> + <div className={S.header}> + {name} + </div> + { selectedCount > 0 ? + <ActionHeader + selectedCount={selectedCount} + allSelected={selectedCount === visibleCount && visibleCount > 0} + setAllSelected={setAllSelected} + /> + : + <SearchHeader searchText={searchText} setSearchText={setSearchText} /> + } + <List entityType={entityType} entityIds={entityIds} setItemSelected={setItemSelected} /> + </div> ); } } diff --git a/frontend/src/questions/duck.js b/frontend/src/questions/duck.js index 424d9c0f6a5..875ba86dcc2 100644 --- a/frontend/src/questions/duck.js +++ b/frontend/src/questions/duck.js @@ -1,45 +1,82 @@ import { AngularResourceProxy, createAction, createThunkAction } from "metabase/lib/redux"; +import { normalize, Schema, arrayOf } from 'normalizr'; + +const card = new Schema('cards'); +// const user = new Schema('users'); +// +// card.define({ +// creator: user +// }); + const CardApi = new AngularResourceProxy("Card", ["list"]); -const SELECT_QUESTION_SECTION = 'metabase/questions/SELECT_QUESTION_SECTION'; +const SELECT_SECTION = 'metabase/questions/SELECT_SECTION'; const SET_SEARCH_TEXT = 'metabase/questions/SET_SEARCH_TEXT'; -const SET_ITEM_CHECKED = 'metabase/questions/SET_ITEM_CHECKED'; +const SET_ITEM_SELECTED = 'metabase/questions/SET_ITEM_SELECTED'; +const SET_ALL_SELECTED = 'metabase/questions/SET_ALL_SELECTED'; -export const selectQuestionSection = createThunkAction(SELECT_QUESTION_SECTION, (section = "all", slug) => { - console.log("section1", section) +export const selectSection = createThunkAction(SELECT_SECTION, (section = "all", slug = null, type = "cards") => { return async (dispatch, getState) => { - console.log("section2", section) - let result; + let response; switch (section) { case "all": - result = await CardApi.list({ filterMode: "all" }); + response = await CardApi.list({ filterMode: "all" }); break; default: - console.log("unknown section " + section); + console.warn("unknown section " + section); } - return { section, slug, result }; + return { type, section, slug, ...normalize(response, arrayOf(card)) }; } }); export const setSearchText = createAction(SET_SEARCH_TEXT); -export const setItemChecked = createAction(SET_ITEM_CHECKED); +export const setItemSelected = createAction(SET_ITEM_SELECTED); +export const setAllSelected = createAction(SET_ALL_SELECTED); const initialState = { - questions: [], + entities: {}, + type: "cards", + section: null, + slug: null, + itemsBySectionId: {}, searchText: "", - checkedItems: {} + selectedIds: {}, + allSelected: false }; export default function(state = initialState, { type, payload, error }) { + if (payload && payload.entities) { + // FIXME: deep merge + state = { + ...state, + entities: { + ...state.entities, + ...payload.entities + } + }; + } + switch (type) { case SET_SEARCH_TEXT: return { ...state, searchText: payload }; - case SET_ITEM_CHECKED: - return { ...state, checkedItems: { ...state.checkedItems, ...payload } }; - case SELECT_QUESTION_SECTION: - return { ...state, questions: payload.result }; + case SET_ITEM_SELECTED: + return { ...state, selectedIds: { ...state.selectedIds, ...payload } }; + case SET_ALL_SELECTED: + return { ...state, selectedIds: {}, allSelected: payload }; + case SELECT_SECTION: + let sectionId = [payload.type, payload.section, payload.slug].join(","); + return { + ...state, + type: payload.type, + section: payload.section, + slug: payload.slug, + itemsBySectionId: { + ...state.itemsBySectionId, + [sectionId]: payload.result + } + }; default: return state; } diff --git a/frontend/src/questions/selectors.js b/frontend/src/questions/selectors.js index a6f8033495b..10f1b11e415 100644 --- a/frontend/src/questions/selectors.js +++ b/frontend/src/questions/selectors.js @@ -4,95 +4,121 @@ import moment from "moment"; import visualizations from "metabase/visualizations"; -export const getQuestions = (state) => state.questions.questions; -export const getSearchText = (state) => state.questions.searchText; -export const getChecked = (state) => state.questions.checkedItems; - -export const getQuestionItems = createSelector( - getQuestions, getChecked, - (questions, checked) => questions.map(q => ({ - name: q.name, - id: q.id, - created: moment(q.created_at).fromNow(), - by: q.creator.common_name, - labels: [], - iconName: (visualizations.get(q.display)||{}).iconName, - checked: checked[q.id] || false - })) -) +function caseInsensitiveSearch(haystack, needle) { + return !needle || (haystack != null && haystack.toLowerCase().indexOf(needle.toLowerCase()) >= 0); +} + +export const getEntityType = (state) => state.questions.type +export const getSection = (state) => state.questions.section +export const getSlug = (state) => state.questions.slug + +export const getSectionId = (state) => [getEntityType(state), getSection(state), getSlug(state)].join(",") +export const getEntities = (state) => state.questions.entities +export const getItemsBySectionId = (state) => state.questions.itemsBySectionId -export const getQuestionItemsFilteredBySearchText = createSelector( - getQuestionItems, getSearchText, - (questionItems, searchText) => questionItems.filter(questionItem => - questionItem.name.toLowerCase().indexOf(searchText.toLowerCase()) >= 0 - ) +// export const getQuestions = (state) => state.questions.questions; +export const getSearchText = (state) => state.questions.searchText; +export const getSelectedIds = (state) => state.questions.selectedIds; +export const getAllSelected = (state) => state.questions.allSelected + +export const getEntityIds = createSelector( + [getSectionId, getItemsBySectionId], + (sectionId, itemsBySectionId) => + itemsBySectionId[sectionId] || [] ); -export const getSections = (state) => ( - [ - { id: "all", name: "All questions", icon: "star", selected: true }, - { id: "favorites", name: "Favorites", icon: "star" }, - { id: "recent", name: "Recently viewed", icon: "star" }, - { id: "saved", name: "Saved by me", icon: "star" }, - { id: "popular", name: "Most popular", icon: "star" } - ] +const getEntity = (state, props) => + getEntities(state)[props.entityType][props.entityId]; + +const getEntitySelected = (state, props) => + getAllSelected(state) || getSelectedIds(state)[props.entityId] || false; + +const getEntityVisible = (state, props) => + caseInsensitiveSearch(getEntity(state, props).name, getSearchText(state)); + +let fakeLabels = []; +const getEntityLabels = (state, props) => { + return fakeLabels; +} + +export const makeGetItem = () => { + const getItem = createSelector( + [getEntity, getEntityLabels, getEntitySelected, getEntityVisible], + (entity, labels, selected, visible) => ({ + name: entity.name, + id: entity.id, + created: moment(entity.created_at).fromNow(), + by: entity.creator.common_name, + icon: (visualizations.get(entity.display)||{}).iconName, + labels, + selected, + visible + }) + ); + return getItem; +} + +const getAllEntities = createSelector( + [getEntityIds, getEntityType, getEntities], + (entityIds, entityType, entities) => + entityIds.map(entityId => entities[entityType][entityId]) ); -export const getTopics = (state) => ( - [ - { id: 0, name: "Revenue", icon: "star", slug: "revenue" }, - { id: 1, name: "Users", icon: "star", slug: "users" }, - { id: 2, name: "Orders", icon: "star", slug: "orders" }, - { id: 3, name: "Shipments", icon: "star", slug: "shipments" } - ] +const getVisibleEntities = createSelector( + [getAllEntities, getSearchText], + (allEntities, searchText) => + allEntities.filter(entity => caseInsensitiveSearch(entity.name, searchText)) ); -export const getLabels = (state) => ( - [ - { id: 1, name: "CATPIs", icon: ":cat:", slug: "catpis"}, - { id: 2, name: "Marketing", icon: "#885AB1", slug: "marketing" }, - { id: 3, name: "Growth", icon: "#F9CF48", slug: "growth" }, - { id: 4, name: "KPIs", icon: "#9CC177", slug: "kpis" }, - { id: 5, name: "Q1", icon: "#ED6E6E", slug: "q1" }, - { id: 6, name: "q2", icon: "#ED6E6E", slug: "q2" }, - { id: 7, name: "All-hands", icon: "#B8A2CC", slug: "all-hands" }, - { id: 9, name: "OLD", icon: "#2D86D4", slug: "old" }, - { id: 10, name: "v2 schema", icon: "#2D86D4", slug: "v2-schema" }, - { id: 11, name: "Rebekah", icon: "#2D86D4", slug: "rebekah" } - ] +const getSelectedEntities = createSelector( + [getVisibleEntities, getSelectedIds, getAllSelected], + (visibleEntities, selectedIds, allSelected) => + visibleEntities.filter(entity => allSelected || selectedIds[entity.id]) +); + +export const getVisibleCount = createSelector( + [getVisibleEntities], + (visibleEntities) => visibleEntities.length +) + +export const getSelectedCount = createSelector( + [getSelectedEntities], + (selectedEntities) => selectedEntities.length ); -// export const questionListSelector = createSelector( -// getQuestionItemsFilteredBySearchText, getSearchText, -// (questionItems, searchText) => ({ -// ...props, -// questions: questionItems, -// searchText: searchText -// }) -// ); -// -// const props = { -// sections: [ -// { id: "all", name: "All questions", icon: "star", selected: true }, -// { id: "favorites", name: "Favorites", icon: "star" }, -// { id: "recent", name: "Recently viewed", icon: "star" }, -// { id: "saved", name: "Saved by me", icon: "star" }, -// { id: "popular", name: "Most popular", icon: "star" } -// ], -// , -// labels: , -// name: "All questions", -// selectedCount: 0 -// }; -// props.questions = [ -// { name: "Maz's great saved question", created: "two weeks ago", by: "Allen Gilliland", type: "pie", labels: [0].map(i => props.labels[i]) }, -// { name: "Revenue by product per week", created: "two weeks ago", by: "Allen Gilliland", type: "bar", labels: [], checked: true }, -// { name: "Avg DAU all time", created: "two weeks ago", by: "Allen Gilliland", type: "bar", labels: [2, 4].map(i => props.labels[i]), favorite: true }, -// { name: "Max 🦠velocity by hour", created: "two weeks ago", by: "Allen Gilliland", type: "line", labels: [] }, -// { name: "All the running Quinnie does late at night. Seriously why does he run so late?", created: "two weeks ago", by: "Allen Gilliland", type: "bar", labels: [0].map(i => props.labels[i]), favorite: true }, -// { name: "Maz's great saved question", created: "two weeks ago", by: "Allen Gilliland", type: "line", labels: [2].map(i => props.labels[i]) }, -// { name: "A map of many things", created: "two weeks ago", by: "Allen Gilliland", type: "pin_map", labels: [] }, -// { name: "Avg DAU all time", created: "two weeks ago", by: "Allen Gilliland", type: "line", labels: [] }, -// { name: "Max 🦠velocity by hour", created: "two weeks ago", by: "Allen Gilliland", type: "line", labels: [2].map(i => props.labels[i]) }, -// { name: "All the running Quinnie does late at night. Seriously why does he run so late?", created: "two weeks ago", by: "Allen Gilliland", type: "scalar", labels: [], favorite: true }, -// ] +// TODO: +export const getSectionName = (state) => + fakeState.sections[0].name; + + +const fakeState = { + sections: [ + { id: "all", name: "All questions", icon: "star", selected: true }, + { id: "favorites", name: "Favorites", icon: "star" }, + { id: "recent", name: "Recently viewed", icon: "star" }, + { id: "saved", name: "Saved by me", icon: "star" }, + { id: "popular", name: "Most popular", icon: "star" } + ], + topics: [ + { id: 0, name: "Revenue", icon: "star", slug: "revenue" }, + { id: 1, name: "Users", icon: "star", slug: "users" }, + { id: 2, name: "Orders", icon: "star", slug: "orders" }, + { id: 3, name: "Shipments", icon: "star", slug: "shipments" } + ], + labels: [ + { id: 1, name: "CATPIs", icon: ":cat:", slug: "catpis"}, + { id: 2, name: "Marketing", icon: "#885AB1", slug: "marketing" }, + { id: 3, name: "Growth", icon: "#F9CF48", slug: "growth" }, + { id: 4, name: "KPIs", icon: "#9CC177", slug: "kpis" }, + { id: 5, name: "Q1", icon: "#ED6E6E", slug: "q1" }, + { id: 6, name: "q2", icon: "#ED6E6E", slug: "q2" }, + { id: 7, name: "All-hands", icon: "#B8A2CC", slug: "all-hands" }, + { id: 9, name: "OLD", icon: "#2D86D4", slug: "old" }, + { id: 10, name: "v2 schema", icon: "#2D86D4", slug: "v2-schema" }, + { id: 11, name: "Rebekah", icon: "#2D86D4", slug: "rebekah" } + ] +} + +export const getSections = (state) => fakeState.sections; +export const getTopics = (state) => fakeState.topics; +export const getLabels = (state) => fakeState.labels; diff --git a/package.json b/package.json index 606925b285f..a3006e86308 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "password-generator": "^2.0.1", "react": "^0.14.8", "react-addons-css-transition-group": "^0.14.7", + "react-addons-perf": "^0.14.8", "react-dom": "^0.14.7", "react-draggable": "^1.1.3", "react-onclickout": "^2.0.4", @@ -45,6 +46,7 @@ "redux": "^3.0.4", "redux-actions": "^0.9.1", "redux-form": "^4.2.0", + "redux-logger": "^2.6.1", "redux-promise": "^0.5.0", "redux-router": "^1.0.0-beta4", "redux-thunk": "^2.0.1", -- GitLab