This is an automated email from the ASF dual-hosted git repository. graceguo pushed a commit to branch scope-selector-modal-v2 in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit 78480273dfecb63f6fbaa867f20f6c6f3726beda Author: Grace <grace....@airbnb.com> AuthorDate: Tue Nov 5 14:28:12 2019 -0800 refactory after design review --- .../ChartIcon.jsx} | 29 +- .../components/filterscope/FilterFieldTree.jsx | 18 +- .../components/filterscope/FilterScopeModal.jsx | 6 +- .../components/filterscope/FilterScopeSelector.jsx | 372 ++++++++++----------- .../components/filterscope/FilterScopeTree.jsx | 15 +- .../filterscope/renderFilterFieldTreeNodes.jsx | 11 +- .../filterscope/renderFilterScopeTreeNodes.jsx | 31 +- .../stylesheets/filter-scope-selector.less | 66 ++-- .../dashboard/util/buildFilterScopeTreeEntry.js | 65 ++++ superset/assets/src/dashboard/util/constants.js | 3 + .../src/dashboard/util/getFilterFieldNodesTree.js | 21 +- .../src/dashboard/util/getFilterScopeNodesTree.js | 132 ++++---- .../dashboard/util/getFilterScopeParentNodes.js | 6 +- ...eParentNodes.js => getKeyForFilterScopeTree.js} | 27 +- .../src/dashboard/util/getRevertedFilterScope.js | 8 +- .../util/getSelectedChartIdForFilterScopeTree.js | 53 +++ 16 files changed, 497 insertions(+), 366 deletions(-) diff --git a/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js b/superset/assets/src/components/ChartIcon.jsx similarity index 61% copy from superset/assets/src/dashboard/util/getFilterScopeParentNodes.js copy to superset/assets/src/components/ChartIcon.jsx index 280661d..d25e453 100644 --- a/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js +++ b/superset/assets/src/components/ChartIcon.jsx @@ -16,24 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -export default function getFilterScopeParentNodes(nodes, depthLimit = 0) { - const parentNodes = []; - const traverse = (currentNode, depth) => { - if (!currentNode) { - return; - } +import React from 'react'; - if (currentNode.children && (depthLimit === 0 || depth < depthLimit)) { - parentNodes.push(currentNode.value); - currentNode.children.forEach(child => traverse(child, depth + 1)); - } - }; +const ChartIcon = () => ( + <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> + <rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#EFF9F9" stroke="#B3DADC" /> + <rect x="8" y="4" width="2" height="10" rx="1" fill="#B3DADC" /> + <rect x="12" y="10" width="2" height="4" rx="1" fill="#B3DADC" /> + <rect x="4" y="6" width="2" height="8" rx="1" fill="#B3DADC" /> + </svg> +); - if (nodes && nodes.length > 0) { - nodes.forEach(node => { - traverse(node, 0); - }); - } - - return parentNodes; -} +export default ChartIcon; diff --git a/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx b/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx index a1b7581..351f539 100644 --- a/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx +++ b/superset/assets/src/dashboard/components/filterscope/FilterFieldTree.jsx @@ -19,6 +19,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import CheckboxTree from 'react-checkbox-tree'; +import { t } from '@superset-ui/translation'; import 'react-checkbox-tree/lib/react-checkbox-tree.css'; import { @@ -30,7 +31,7 @@ import renderFilterFieldTreeNodes from './renderFilterFieldTreeNodes'; import { filterScopeSelectorTreeNodePropShape } from '../../util/propShapes'; const propTypes = { - activeKey: PropTypes.string.isRequired, + activeKey: PropTypes.string, nodes: PropTypes.arrayOf(filterScopeSelectorTreeNodePropShape).isRequired, checked: PropTypes.arrayOf( PropTypes.oneOfType([PropTypes.number, PropTypes.string]), @@ -43,21 +44,29 @@ const propTypes = { onClick: PropTypes.func.isRequired, }; +const defaultProps = { + activeKey: '', +}; + const FILTER_FIELD_CHECKBOX_TREE_ICONS = { check: <CheckboxChecked />, uncheck: <CheckboxUnchecked />, halfCheck: <CheckboxHalfChecked />, expandClose: <span className="rct-icon rct-icon-expand-close" />, expandOpen: <span className="rct-icon rct-icon-expand-open" />, - expandAll: <span className="rct-icon rct-icon-expand-all" />, - collapseAll: <span className="rct-icon rct-icon-collapse-all" />, + expandAll: ( + <span className="rct-icon rct-icon-expand-all">{t('Expand all')}</span> + ), + collapseAll: ( + <span className="rct-icon rct-icon-collapse-all">{t('Collapse all')}</span> + ), parentClose: <span className="rct-icon rct-icon-parent-close" />, parentOpen: <span className="rct-icon rct-icon-parent-open" />, leaf: <span className="rct-icon rct-icon-leaf" />, }; export default function FilterFieldTree({ - activeKey = '', + activeKey, nodes = [], checked = [], expanded = [], @@ -82,3 +91,4 @@ export default function FilterFieldTree({ } FilterFieldTree.propTypes = propTypes; +FilterFieldTree.defaultProps = defaultProps; diff --git a/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx b/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx index ba2b153..b2a8be8 100644 --- a/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx +++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeModal.jsx @@ -31,10 +31,10 @@ export default class FilterScopeModal extends React.PureComponent { super(props); this.modal = React.createRef(); - this.close = this.close.bind(this); + this.handleCloseModal = this.handleCloseModal.bind(this); } - close() { + handleCloseModal() { if (this.modal) { this.modal.current.close(); } @@ -48,7 +48,7 @@ export default class FilterScopeModal extends React.PureComponent { triggerNode={this.props.triggerNode} modalBody={ <div className="dashboard-modal filter-scope"> - <FilterScope onCloseModal={this.close} /> + <FilterScope onCloseModal={this.handleCloseModal} /> </div> } /> diff --git a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx index 855e66d..f20044e 100644 --- a/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx +++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeSelector.jsx @@ -21,12 +21,14 @@ import PropTypes from 'prop-types'; import cx from 'classnames'; import { Button } from 'react-bootstrap'; import { t } from '@superset-ui/translation'; -import { safeStringify } from '../../../utils/safeStringify'; +import buildFilterScopeTreeEntry from '../../util/buildFilterScopeTreeEntry'; import getFilterScopeNodesTree from '../../util/getFilterScopeNodesTree'; import getFilterFieldNodesTree from '../../util/getFilterFieldNodesTree'; import getFilterScopeParentNodes from '../../util/getFilterScopeParentNodes'; import getCurrentScopeChartIds from '../../util/getCurrentScopeChartIds'; +import getKeyForFilterScopeTree from '../../util/getKeyForFilterScopeTree'; +import getSelectedChartIdForFilterScopeTree from '../../util/getSelectedChartIdForFilterScopeTree'; import getRevertedFilterScope from '../../util/getRevertedFilterScope'; import FilterScopeTree from './FilterScopeTree'; import FilterFieldTree from './FilterFieldTree'; @@ -34,6 +36,7 @@ import { getChartIdAndColumnFromFilterKey, getDashboardFilterKey, } from '../../util/getDashboardFilterKey'; +import { ALL_FILTERS } from '../../util/constants'; const propTypes = { dashboardFilters: PropTypes.object.isRequired, @@ -59,26 +62,19 @@ export default class FilterScopeSelector extends React.PureComponent { // display filter fields in tree structure const filterFieldNodes = getFilterFieldNodesTree({ dashboardFilters, - isSingleEditMode: true, }); + // filterFieldNodes root node is dashboard_root component, + // so that we can offer a select/deselect all link + const filtersNodes = filterFieldNodes[0].children; this.allfilterFields = []; - filterFieldNodes.forEach(({ children }) => { + filtersNodes.forEach(({ children }) => { children.forEach(child => { this.allfilterFields.push(child.value); }); }); + this.defaultFilterKey = filtersNodes[0].children[0].value; - if (filterFieldNodes.length && filterFieldNodes[0].children) { - this.defaultFilterKey = filterFieldNodes[0].children[0].value; - } - const checkedFilterFields = [this.defaultFilterKey]; - // expand defaultFilterKey - const { chartId } = getChartIdAndColumnFromFilterKey( - this.defaultFilterKey, - ); - const expandedFilterIds = [chartId]; - - // display checkbox tree of whole dashboard layout + // build FilterScopeTree object for each filterKey const filterScopeMap = Object.values(dashboardFilters).reduce( (map, { chartId: filterId, columns }) => { const filterScopeByChartId = Object.keys(columns).reduce( @@ -89,8 +85,7 @@ export default class FilterScopeSelector extends React.PureComponent { }); const nodes = getFilterScopeNodesTree({ components: layout, - isSingleEditMode: true, - checkedFilterFields, + filterFields: [filterKey], selectedChartId: filterId, }); const checked = getCurrentScopeChartIds({ @@ -107,7 +102,7 @@ export default class FilterScopeSelector extends React.PureComponent { // unfiltered nodes nodes, // filtered nodes in display if searchText is not empty - nodesFiltered: nodes.slice(), + nodesFiltered: [...nodes], checked, expanded, }, @@ -124,15 +119,32 @@ export default class FilterScopeSelector extends React.PureComponent { {}, ); + // initial state: active defaultFilerKey + const { chartId } = getChartIdAndColumnFromFilterKey( + this.defaultFilterKey, + ); + const checkedFilterFields = []; + const activeFilterField = this.defaultFilterKey; + // expand defaultFilterKey in filter field tree + const expandedFilterIds = [ALL_FILTERS].concat(chartId); + + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ + checkedFilterFields, + activeFilterField, + filterScopeMap, + layout, + }); this.state = { showSelector: true, - activeKey: this.defaultFilterKey, + activeFilterField, searchText: '', - filterScopeMap, + filterScopeMap: { + ...filterScopeMap, + ...filterScopeTreeEntry, + }, filterFieldNodes, checkedFilterFields, expandedFilterIds, - isSingleEditMode: true, }; } else { this.state = { @@ -142,7 +154,6 @@ export default class FilterScopeSelector extends React.PureComponent { this.filterNodes = this.filterNodes.bind(this); this.onChangeFilterField = this.onChangeFilterField.bind(this); - this.onToggleEditMode = this.onToggleEditMode.bind(this); this.onCheckFilterScope = this.onCheckFilterScope.bind(this); this.onExpandFilterScope = this.onExpandFilterScope.bind(this); this.onSearchInputChange = this.onSearchInputChange.bind(this); @@ -154,88 +165,76 @@ export default class FilterScopeSelector extends React.PureComponent { onCheckFilterScope(checked = []) { const { - activeKey, + activeFilterField, filterScopeMap, - isSingleEditMode, checkedFilterFields, } = this.state; - if (isSingleEditMode) { - const updatedEntry = { - ...filterScopeMap[activeKey], - checked: checked.map(c => parseInt(c, 10)), - }; - - this.setState(() => ({ - filterScopeMap: { - ...filterScopeMap, - [activeKey]: updatedEntry, - }, - })); - } else { - // multi edit mode: update every scope in checkedFilterFields based on grouped selection - const updatedEntry = { - ...filterScopeMap[activeKey], - checked, - }; + const key = getKeyForFilterScopeTree({ + activeFilterField, + checkedFilterFields, + }); + const editingList = activeFilterField + ? [activeFilterField] + : checkedFilterFields; + const updatedEntry = { + ...filterScopeMap[key], + checked, + }; - const updatedFilterScopeMap = getRevertedFilterScope({ - checked, - checkedFilterFields, - filterScopeMap, - }); + const updatedFilterScopeMap = getRevertedFilterScope({ + checked, + filterFields: editingList, + filterScopeMap, + }); - this.setState(() => ({ - filterScopeMap: { - ...filterScopeMap, - ...updatedFilterScopeMap, - [activeKey]: updatedEntry, - }, - })); - } + this.setState(() => ({ + filterScopeMap: { + ...filterScopeMap, + ...updatedFilterScopeMap, + [key]: updatedEntry, + }, + })); } onExpandFilterScope(expanded = []) { - const { activeKey, filterScopeMap } = this.state; + const { + activeFilterField, + checkedFilterFields, + filterScopeMap, + } = this.state; + const key = getKeyForFilterScopeTree({ + activeFilterField, + checkedFilterFields, + }); const updatedEntry = { - ...filterScopeMap[activeKey], + ...filterScopeMap[key], expanded, }; this.setState(() => ({ filterScopeMap: { ...filterScopeMap, - [activeKey]: updatedEntry, + [key]: updatedEntry, }, })); } onCheckFilterField(checkedFilterFields = []) { const { layout } = this.props; - const { isSingleEditMode, filterScopeMap } = this.state; - const nodes = getFilterScopeNodesTree({ - components: layout, - isSingleEditMode, + const { filterScopeMap } = this.state; + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ checkedFilterFields, - }); - const activeKey = safeStringify(checkedFilterFields); - const checkedChartIdSet = new Set(); - checkedFilterFields.forEach(filterField => { - (filterScopeMap[filterField].checked || []).forEach(chartId => { - checkedChartIdSet.add(`${chartId}:${filterField}`); - }); + activeFilterField: '', + filterScopeMap, + layout, }); this.setState(() => ({ - activeKey, + activeFilterField: '', checkedFilterFields, filterScopeMap: { ...filterScopeMap, - [activeKey]: { - nodes, - nodesFiltered: [...nodes], - checked: [...checkedChartIdSet], - expanded: getFilterScopeParentNodes(nodes, 1), - }, + ...filterScopeTreeEntry, }, })); } @@ -246,64 +245,58 @@ export default class FilterScopeSelector extends React.PureComponent { })); } - onSearchInputChange(e) { - this.setState({ searchText: e.target.value }, this.filterTree); - } - onChangeFilterField(filterField = {}) { - const nextActiveKey = filterField.value; + const { layout } = this.props; + const nextActiveFilterField = filterField.value; const { - isSingleEditMode, - activeKey: currentActiveKey, + activeFilterField: currentActiveFilterField, checkedFilterFields, + filterScopeMap, } = this.state; - // multi-edit mode: if user click on the single filter field, + // we allow single edit and multiple edit in the same view. + // if user click on the single filter field, // will show filter scope for the single field. // if user click on the same filter filed again, - // will toggle activeKey back to group selected fields - if (!isSingleEditMode && nextActiveKey === currentActiveKey) { - this.onCheckFilterField(checkedFilterFields); - } else if (this.allfilterFields.includes(nextActiveKey)) { - this.setState({ activeKey: nextActiveKey }); - } - } + // will toggle off the single filter field, + // and allow multi-edit all checked filter fields. + if (nextActiveFilterField === currentActiveFilterField) { + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ + checkedFilterFields, + activeFilterField: '', + filterScopeMap, + layout, + }); - onToggleEditMode() { - const { activeKey, isSingleEditMode, checkedFilterFields } = this.state; - const { dashboardFilters } = this.props; - if (isSingleEditMode) { - // single edit => multi edit - this.setState( - { - isSingleEditMode: false, - checkedFilterFields: [activeKey], - filterFieldNodes: getFilterFieldNodesTree({ - dashboardFilters, - isSingleEditMode: false, - }), + this.setState({ + activeFilterField: '', + filterScopeMap: { + ...filterScopeMap, + ...filterScopeTreeEntry, }, - () => this.onCheckFilterField([activeKey]), - ); - } else { - // multi edit => single edit - const nextActiveKey = - checkedFilterFields.length === 0 - ? this.defaultFilterKey - : checkedFilterFields[0]; - - this.setState(() => ({ - isSingleEditMode: true, - activeKey: nextActiveKey, - checkedFilterFields: [activeKey], - filterFieldNodes: getFilterFieldNodesTree({ - dashboardFilters, - isSingleEditMode: true, - }), - })); + }); + } else if (this.allfilterFields.includes(nextActiveFilterField)) { + const filterScopeTreeEntry = buildFilterScopeTreeEntry({ + checkedFilterFields, + activeFilterField: nextActiveFilterField, + filterScopeMap, + layout, + }); + + this.setState({ + activeFilterField: nextActiveFilterField, + filterScopeMap: { + ...filterScopeMap, + ...filterScopeTreeEntry, + }, + }); } } + onSearchInputChange(e) { + this.setState({ searchText: e.target.value }, this.filterTree); + } + onClose() { this.props.onCloseModal(); } @@ -321,40 +314,62 @@ export default class FilterScopeSelector extends React.PureComponent { {}, ), ); - this.props.onCloseModal(); + + // save do not close modal } filterTree() { // Reset nodes back to unfiltered state if (!this.state.searchText) { this.setState(prevState => { - const { activeKey, filterScopeMap } = prevState; + const { + activeFilterField, + checkedFilterFields, + filterScopeMap, + } = prevState; + const key = getKeyForFilterScopeTree({ + activeFilterField, + checkedFilterFields, + }); + const updatedEntry = { - ...filterScopeMap[activeKey], - nodesFiltered: filterScopeMap[activeKey].nodes, + ...filterScopeMap[key], + nodesFiltered: filterScopeMap[key].nodes, }; return { filterScopeMap: { ...filterScopeMap, - [activeKey]: updatedEntry, + [key]: updatedEntry, }, }; }); } else { const updater = prevState => { - const { activeKey, filterScopeMap } = prevState; - const nodesFiltered = filterScopeMap[activeKey].nodes.reduce( + const { + activeFilterField, + checkedFilterFields, + filterScopeMap, + } = prevState; + const key = getKeyForFilterScopeTree({ + activeFilterField, + checkedFilterFields, + }); + + const nodesFiltered = filterScopeMap[key].nodes.reduce( this.filterNodes, [], ); + const expanded = getFilterScopeParentNodes([...nodesFiltered]); const updatedEntry = { - ...filterScopeMap[activeKey], + ...filterScopeMap[key], nodesFiltered, + expanded, }; + return { filterScopeMap: { ...filterScopeMap, - [activeKey]: updatedEntry, + [key]: updatedEntry, }, }; }; @@ -382,14 +397,14 @@ export default class FilterScopeSelector extends React.PureComponent { renderFilterFieldList() { const { - activeKey, + activeFilterField, filterFieldNodes, checkedFilterFields, expandedFilterIds, } = this.state; return ( <FilterFieldTree - activeKey={activeKey} + activeKey={activeFilterField} nodes={filterFieldNodes} checked={checkedFilterFields} expanded={expandedFilterIds} @@ -403,100 +418,85 @@ export default class FilterScopeSelector extends React.PureComponent { renderFilterScopeTree() { const { filterScopeMap, - activeKey, - isSingleEditMode, + activeFilterField, + checkedFilterFields, searchText, } = this.state; - const selectedFilterId = isSingleEditMode - ? getChartIdAndColumnFromFilterKey(activeKey).chartId - : 0; + const key = getKeyForFilterScopeTree({ + activeFilterField, + checkedFilterFields, + }); + + const selectedChartId = getSelectedChartIdForFilterScopeTree({ + activeFilterField, + checkedFilterFields, + }); return ( <React.Fragment> <input - className={cx('filter-text scope-search', { - 'multi-edit-mode': !isSingleEditMode, - })} + className="filter-text scope-search multi-edit-mode" placeholder={t('Search...')} type="text" value={searchText} onChange={this.onSearchInputChange} /> <FilterScopeTree - nodes={filterScopeMap[activeKey].nodesFiltered} - checked={filterScopeMap[activeKey].checked} - expanded={filterScopeMap[activeKey].expanded} + nodes={filterScopeMap[key].nodesFiltered} + checked={filterScopeMap[key].checked} + expanded={filterScopeMap[key].expanded} onCheck={this.onCheckFilterScope} onExpand={this.onExpandFilterScope} // pass selectedFilterId prop to FilterScopeTree component, // to hide checkbox for selected filter field itself - selectedFilterId={selectedFilterId} + selectedChartId={selectedChartId} /> </React.Fragment> ); } - renderEditModeControl() { - const { isSingleEditMode } = this.state; - return ( - <span - role="button" - tabIndex="0" - className="edit-mode-toggle" - onClick={this.onToggleEditMode} - > - {isSingleEditMode - ? t('Edit multiple filters') - : t('Edit individual filter')} - </span> - ); - } - - render() { + renderEditingFiltersName() { const { dashboardFilters } = this.props; - const { showSelector, isSingleEditMode, activeKey } = this.state; - const isSingleValue = activeKey.indexOf('[') === -1; + const { activeFilterField, checkedFilterFields } = this.state; const currentFilterLabels = [] - .concat(isSingleValue ? activeKey : JSON.parse(activeKey)) + .concat(activeFilterField || checkedFilterFields) .map(key => { const { chartId, column } = getChartIdAndColumnFromFilterKey(key); return dashboardFilters[chartId].labels[column] || column; }); return ( + <div className="selected-fields multi-edit-mode"> + {currentFilterLabels.length === 0 && t('No filters are selected.')} + {currentFilterLabels.length === 1 && t('Editing 1 filter: ')} + {currentFilterLabels.length > 1 && + t('Batch editing %d filters: ', currentFilterLabels.length)} + <span className="selected-scopes"> + {currentFilterLabels.join(', ')} + </span> + </div> + ); + } + + render() { + const { showSelector } = this.state; + + return ( <React.Fragment> <div className="filter-scope-container"> <div className="filter-scope-header"> <h4>{t('Configure filter scopes')}</h4> - <div - className={cx('selected-fields', { - 'multi-edit-mode': !isSingleEditMode, - })} - > - {`Batch editing ${currentFilterLabels.length} filters: `} - <span className="selected-scopes"> - {currentFilterLabels.join(', ')} - </span> - </div> + {this.renderEditingFiltersName()} </div> {!showSelector ? ( - <div>{t('There are no filter in this dashboard')}</div> + <div>{t('There are no filters in this dashboard')}</div> ) : ( <div className="filters-scope-selector"> - <div - className={cx('filter-field-pane', { - 'multi-edit-mode': !isSingleEditMode, - })} - > - {this.renderEditModeControl()} + <div className={cx('filter-field-pane multi-edit-mode')}> {this.renderFilterFieldList()} </div> - <div - className={cx('filter-scope-pane', { - 'multi-edit-mode': !isSingleEditMode, - })} - > + <div className="filter-scope-pane multi-edit-mode"> {this.renderFilterScopeTree()} </div> </div> diff --git a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx b/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx index 08769ba..b713362 100644 --- a/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx +++ b/superset/assets/src/dashboard/components/filterscope/FilterScopeTree.jsx @@ -20,6 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import CheckboxTree from 'react-checkbox-tree'; import 'react-checkbox-tree/lib/react-checkbox-tree.css'; +import { t } from '@superset-ui/translation'; import { CheckboxChecked, @@ -39,7 +40,7 @@ const propTypes = { ).isRequired, onCheck: PropTypes.func.isRequired, onExpand: PropTypes.func.isRequired, - selectedFilterId: PropTypes.number.isRequired, + selectedChartId: PropTypes.number.isRequired, }; const NOOP = () => {}; @@ -50,8 +51,12 @@ const FILTER_SCOPE_CHECKBOX_TREE_ICONS = { halfCheck: <CheckboxHalfChecked />, expandClose: <span className="rct-icon rct-icon-expand-close" />, expandOpen: <span className="rct-icon rct-icon-expand-open" />, - expandAll: <span className="rct-icon rct-icon-expand-all" />, - collapseAll: <span className="rct-icon rct-icon-collapse-all" />, + expandAll: ( + <span className="rct-icon rct-icon-expand-all">{t('Expand all')}</span> + ), + collapseAll: ( + <span className="rct-icon rct-icon-collapse-all">{t('Collapse all')}</span> + ), parentClose: <span className="rct-icon rct-icon-parent-close" />, parentOpen: <span className="rct-icon rct-icon-parent-open" />, leaf: <span className="rct-icon rct-icon-leaf" />, @@ -63,14 +68,14 @@ export default function FilterScopeTree({ expanded = [], onCheck, onExpand, - selectedFilterId = 0, + selectedChartId = 0, }) { return ( <CheckboxTree showExpandAll expandOnClick showNodeIcon={false} - nodes={renderFilterScopeTreeNodes({ nodes, selectedFilterId })} + nodes={renderFilterScopeTreeNodes({ nodes, selectedChartId })} checked={checked} expanded={expanded} onCheck={onCheck} diff --git a/superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx b/superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx index ac9e9b1..2cfec5a 100644 --- a/superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx +++ b/superset/assets/src/dashboard/components/filterscope/renderFilterFieldTreeNodes.jsx @@ -25,7 +25,9 @@ export default function renderFilterFieldTreeNodes({ nodes = [], activeKey = '', }) { - return nodes.map(node => ({ + const root = nodes[0]; + const allFilterNodes = nodes[0].children; + const children = allFilterNodes.map(node => ({ ...node, children: node.children.map(child => { const { label, value } = child; @@ -42,4 +44,11 @@ export default function renderFilterFieldTreeNodes({ }; }), })); + + return [ + { + ...root, + children, + }, + ]; } diff --git a/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx b/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx index a4982b7..b08a207 100644 --- a/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx +++ b/superset/assets/src/dashboard/components/filterscope/renderFilterScopeTreeNodes.jsx @@ -18,26 +18,32 @@ */ import React from 'react'; import cx from 'classnames'; -import { t } from '@superset-ui/translation'; -import { DASHBOARD_ROOT_TYPE } from '../../util/componentTypes'; +import ChartIcon from '../../../components/ChartIcon'; +import { CHART_TYPE } from '../../util/componentTypes'; -function traverse({ currentNode, selectedFilterId }) { +function traverse({ currentNode, selectedChartId }) { if (!currentNode) { return null; } - const { label, type, children } = currentNode; + const { label, value, type, children } = currentNode; if (children && children.length) { const updatedChildren = children.map(child => - traverse({ currentNode: child, selectedFilterId }), + traverse({ currentNode: child, selectedChartId }), ); return { ...currentNode, label: ( - <a className={`filter-scope-type ${type.toLowerCase()}`}> - {type !== DASHBOARD_ROOT_TYPE && ( - <span className="type-indicator">{t(type)}</span> + <a + className={cx(`filter-scope-type ${type.toLowerCase()}`, { + 'selected-filter': selectedChartId === value, + })} + > + {type === CHART_TYPE && ( + <span className="type-indicator"> + <ChartIcon /> + </span> )} {label} </a> @@ -45,17 +51,14 @@ function traverse({ currentNode, selectedFilterId }) { children: updatedChildren, }; } - - const { value } = currentNode; return { ...currentNode, label: ( <a className={cx(`filter-scope-type ${type.toLowerCase()}`, { - 'selected-filter': selectedFilterId === value, + 'selected-filter': selectedChartId === value, })} > - <span className="type-indicator">{t(type)}</span> {label} </a> ), @@ -64,7 +67,7 @@ function traverse({ currentNode, selectedFilterId }) { export default function renderFilterScopeTreeNodes({ nodes = [], - selectedFilterId = 0, + selectedChartId = 0, }) { - return nodes.map(node => traverse({ currentNode: node, selectedFilterId })); + return nodes.map(node => traverse({ currentNode: node, selectedChartId })); } diff --git a/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less b/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less index cef4083..9f3822b 100644 --- a/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less +++ b/superset/assets/src/dashboard/stylesheets/filter-scope-selector.less @@ -42,7 +42,7 @@ } .filters-scope-selector { - margin: 10px -24px 20px -24px; + margin: 10px -24px 20px; display: flex; flex-direction: row; position: relative; @@ -55,13 +55,16 @@ text-decoration: none; } - .filter-field-pane .edit-mode-toggle, .react-checkbox-tree .rct-icon.rct-icon-expand-all, .react-checkbox-tree .rct-icon.rct-icon-collapse-all { font-size: 13px; font-family: @font-family-sans-serif; color: @brand-primary; + &::before { + content: ''; + } + &:hover { text-decoration: underline; } @@ -77,11 +80,6 @@ padding: 16px 16px 16px 24px; border-right: 1px solid #ccc; - .edit-mode-toggle { - position: absolute; - right: 24px; - } - .filter-container { label { font-weight: normal; @@ -90,7 +88,7 @@ } .filter-field-item { - height: 40px; + height: 35px; display: flex; align-items: center; padding: 0 24px; @@ -105,17 +103,8 @@ } .react-checkbox-tree { - ol ol { - padding: 0; - } - - .rct-bare-label { - font-weight: bold; - } - .rct-text { - margin: 8px 0; - height: 30px; + height: 40px; } } } @@ -136,12 +125,9 @@ display: block; .type-indicator { - border: 1px solid @gray-light; - border-radius: 4px; - padding: 2px 4px; - font-size: 10px; + position: relative; + top: 3px; margin-right: 8px; - font-weight: 400; } &.chart { @@ -150,6 +136,21 @@ &.selected-filter { padding-left: 28px; + position: relative; + color: #aaa; + + &::before { + content: " "; + position: absolute; + left: 0; + top: 50%; + width: 18px; + height: 18px; + border-radius: 2px; + margin-top: -9px; + box-shadow: inset 0 0 0 2px #ccc; + background: #f2f2f2; + } } &.root { @@ -177,23 +178,10 @@ } } - .rct-option .rct-icon { - &.rct-icon-expand-all { - &::before { - content: 'Expand all'; - } - } - - &.rct-icon-collapse-all { - &::before { - content: 'Collapse all'; - } - } - } - .rct-options { text-align: left; margin-left: 0; + margin-bottom: 8px; } .rct-text { @@ -222,10 +210,6 @@ } } - &.filter-text { - display: none; - } - .filter-field-item { padding: 0 16px 0 50px; margin-left: -50px; diff --git a/superset/assets/src/dashboard/util/buildFilterScopeTreeEntry.js b/superset/assets/src/dashboard/util/buildFilterScopeTreeEntry.js new file mode 100644 index 0000000..318a0f6 --- /dev/null +++ b/superset/assets/src/dashboard/util/buildFilterScopeTreeEntry.js @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import getFilterScopeNodesTree from './getFilterScopeNodesTree'; +import getFilterScopeParentNodes from './getFilterScopeParentNodes'; +import getKeyForFilterScopeTree from './getKeyForFilterScopeTree'; +import getSelectedChartIdForFilterScopeTree from './getSelectedChartIdForFilterScopeTree'; + +export default function buildFilterScopeTreeEntry({ + checkedFilterFields = [], + activeFilterField = '', + filterScopeMap = {}, + layout = {}, +}) { + const key = getKeyForFilterScopeTree({ + checkedFilterFields, + activeFilterField, + }); + const editingList = activeFilterField + ? [activeFilterField] + : checkedFilterFields; + const selectedChartId = getSelectedChartIdForFilterScopeTree({ + checkedFilterFields, + activeFilterField, + }); + const nodes = getFilterScopeNodesTree({ + components: layout, + filterFields: editingList, + selectedChartId, + }); + const checkedChartIdSet = new Set(); + editingList.forEach(filterField => { + (filterScopeMap[filterField].checked || []).forEach(chartId => { + checkedChartIdSet.add(`${chartId}:${filterField}`); + }); + }); + const checked = [...checkedChartIdSet]; + const expanded = filterScopeMap[key] + ? filterScopeMap[key].expanded + : getFilterScopeParentNodes(nodes, 1); + + return { + [key]: { + nodes, + nodesFiltered: [...nodes], + checked, + expanded, + }, + }; +} diff --git a/superset/assets/src/dashboard/util/constants.js b/superset/assets/src/dashboard/util/constants.js index e2cbd32..25f9474 100644 --- a/superset/assets/src/dashboard/util/constants.js +++ b/superset/assets/src/dashboard/util/constants.js @@ -76,3 +76,6 @@ export const FILTER_INDICATORS_DISPLAY_LENGTH = 3; // in-component element types: can be added into // directPathToChild, used for in dashboard navigation and focus export const IN_COMPONENT_ELEMENT_TYPES = ['LABEL']; + +// filter scope selector filter fields pane root id +export const ALL_FILTERS = 'ALL_FILTERS'; diff --git a/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js b/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js index d82ea88..4ec9817 100644 --- a/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js +++ b/superset/assets/src/dashboard/util/getFilterFieldNodesTree.js @@ -16,24 +16,31 @@ * specific language governing permissions and limitations * under the License. */ +import { t } from '@superset-ui/translation'; + import { getDashboardFilterKey } from './getDashboardFilterKey'; +import { ALL_FILTERS } from './constants'; -export default function getFilterFieldNodesTree({ - dashboardFilters = {}, - isSingleEditMode = true, -}) { - return Object.values(dashboardFilters).map(dashboardFilter => { +export default function getFilterFieldNodesTree({ dashboardFilters = {} }) { + const allFilters = Object.values(dashboardFilters).map(dashboardFilter => { const { chartId, filterName, columns, labels } = dashboardFilter; const children = Object.keys(columns).map(column => ({ value: getDashboardFilterKey({ chartId, column }), label: labels[column] || column, - showCheckbox: !isSingleEditMode, })); return { value: chartId, label: filterName, children, - showCheckbox: !isSingleEditMode, + showCheckbox: true, }; }); + + return [ + { + value: ALL_FILTERS, + label: t('Select/deselect all filters'), + children: allFilters, + }, + ]; } diff --git a/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js b/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js index 740ac0d..4d4279c 100644 --- a/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js +++ b/superset/assets/src/dashboard/util/getFilterScopeNodesTree.js @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { isEmpty } from 'lodash'; + import { DASHBOARD_ROOT_ID } from './constants'; import { CHART_TYPE, @@ -25,83 +27,93 @@ import { const FILTER_SCOPE_CONTAINER_TYPES = [TAB_TYPE, DASHBOARD_ROOT_TYPE]; -export default function getFilterScopeNodesTree({ +function traverse({ + currentNode = {}, components = {}, - isSingleEditMode = true, - checkedFilterFields = [], - selectedChartId, + filterFields = [], + selectedChartId = 0, }) { - function traverse(currentNode) { - if (!currentNode) { - return null; - } - - const type = currentNode.type; - if (CHART_TYPE === type && currentNode.meta.chartId) { - const chartNode = { - value: currentNode.meta.chartId, - label: - currentNode.meta.sliceName || `${type} ${currentNode.meta.chartId}`, - type, - showCheckbox: selectedChartId !== currentNode.meta.chartId, - }; - - if (isSingleEditMode) { - return chartNode; - } + if (!currentNode) { + return null; + } - return { - ...chartNode, - children: checkedFilterFields.map(filterField => ({ - value: `${currentNode.meta.chartId}:${filterField}`, - label: `${currentNode.meta.chartId}:${filterField}`, - type: 'filter_box', - showCheckbox: false, - })), - }; - } + const type = currentNode.type; + if (CHART_TYPE === type && currentNode.meta.chartId) { + const chartNode = { + value: currentNode.meta.chartId, + label: + currentNode.meta.sliceName || `${type} ${currentNode.meta.chartId}`, + type, + showCheckbox: selectedChartId !== currentNode.meta.chartId, + }; - let children = []; - if (currentNode.children && currentNode.children.length) { - currentNode.children.forEach(child => { - const childNodeTree = traverse(components[child]); + return { + ...chartNode, + children: filterFields.map(filterField => ({ + value: `${currentNode.meta.chartId}:${filterField}`, + label: `${chartNode.label}`, + type: 'filter_box', + showCheckbox: false, + })), + }; + } - const childType = components[child].type; - if (FILTER_SCOPE_CONTAINER_TYPES.includes(childType)) { - children.push(childNodeTree); - } else { - children = children.concat(childNodeTree); - } + let children = []; + if (currentNode.children && currentNode.children.length) { + currentNode.children.forEach(child => { + const childNodeTree = traverse({ + currentNode: components[child], + components, + filterFields, + selectedChartId, }); - } - if (FILTER_SCOPE_CONTAINER_TYPES.includes(type)) { - let label = ''; - if (type === DASHBOARD_ROOT_TYPE) { - label = 'All dashboard'; + const childType = components[child].type; + if (FILTER_SCOPE_CONTAINER_TYPES.includes(childType)) { + children.push(childNodeTree); } else { - label = - currentNode.meta && currentNode.meta.text - ? currentNode.meta.text - : `${type} ${currentNode.id}`; + children = children.concat(childNodeTree); } + }); + } - return { - value: currentNode.id, - label, - type, - children, - }; + if (FILTER_SCOPE_CONTAINER_TYPES.includes(type)) { + let label = ''; + if (type === DASHBOARD_ROOT_TYPE) { + label = 'Select/deselect all charts'; + } else { + label = + currentNode.meta && currentNode.meta.text + ? currentNode.meta.text + : `${type} ${currentNode.id}`; } - return children; + return { + value: currentNode.id, + label, + type, + children, + }; } - if (Object.keys(components).length === 0) { + return children; +} + +export default function getFilterScopeNodesTree({ + components = {}, + filterFields = [], + selectedChartId = 0, +}) { + if (isEmpty(components)) { return []; } - const root = traverse(components[DASHBOARD_ROOT_ID]); + const root = traverse({ + currentNode: components[DASHBOARD_ROOT_ID], + components, + filterFields, + selectedChartId, + }); return [ { ...root, diff --git a/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js b/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js index 280661d..8330c64 100644 --- a/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js +++ b/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js @@ -16,20 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -export default function getFilterScopeParentNodes(nodes, depthLimit = 0) { +export default function getFilterScopeParentNodes(nodes = [], depthLimit = -1) { const parentNodes = []; const traverse = (currentNode, depth) => { if (!currentNode) { return; } - if (currentNode.children && (depthLimit === 0 || depth < depthLimit)) { + if (currentNode.children && (depthLimit === -1 || depth < depthLimit)) { parentNodes.push(currentNode.value); currentNode.children.forEach(child => traverse(child, depth + 1)); } }; - if (nodes && nodes.length > 0) { + if (nodes.length > 0) { nodes.forEach(node => { traverse(node, 0); }); diff --git a/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js b/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js similarity index 61% copy from superset/assets/src/dashboard/util/getFilterScopeParentNodes.js copy to superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js index 280661d..e85dd51 100644 --- a/superset/assets/src/dashboard/util/getFilterScopeParentNodes.js +++ b/superset/assets/src/dashboard/util/getKeyForFilterScopeTree.js @@ -16,24 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -export default function getFilterScopeParentNodes(nodes, depthLimit = 0) { - const parentNodes = []; - const traverse = (currentNode, depth) => { - if (!currentNode) { - return; - } +import { safeStringify } from '../../utils/safeStringify'; - if (currentNode.children && (depthLimit === 0 || depth < depthLimit)) { - parentNodes.push(currentNode.value); - currentNode.children.forEach(child => traverse(child, depth + 1)); - } - }; - - if (nodes && nodes.length > 0) { - nodes.forEach(node => { - traverse(node, 0); - }); - } - - return parentNodes; +export default function getKeyForFilterScopeTree({ + activeFilterField, + checkedFilterFields, +}) { + return activeFilterField + ? safeStringify([activeFilterField]) + : safeStringify(checkedFilterFields); } diff --git a/superset/assets/src/dashboard/util/getRevertedFilterScope.js b/superset/assets/src/dashboard/util/getRevertedFilterScope.js index f8fe550..92e4a29 100644 --- a/superset/assets/src/dashboard/util/getRevertedFilterScope.js +++ b/superset/assets/src/dashboard/util/getRevertedFilterScope.js @@ -17,9 +17,9 @@ * under the License. */ export default function getRevertedFilterScope({ - checked, - checkedFilterFields, - filterScopeMap, + checked = [], + filterFields = [], + filterScopeMap = {}, }) { const checkedChartIdsByFilterField = checked.reduce((map, value) => { const [chartId, filterField] = value.split(':'); @@ -29,7 +29,7 @@ export default function getRevertedFilterScope({ }; }, {}); - return checkedFilterFields.reduce( + return filterFields.reduce( (map, filterField) => ({ ...map, [filterField]: { diff --git a/superset/assets/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js b/superset/assets/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js new file mode 100644 index 0000000..257ec2e --- /dev/null +++ b/superset/assets/src/dashboard/util/getSelectedChartIdForFilterScopeTree.js @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { getChartIdAndColumnFromFilterKey } from './getDashboardFilterKey'; + +export default function getSelectedChartIdForFilterScopeTree({ + activeFilterField, + checkedFilterFields, +}) { + // we don't apply filter on filter_box itself, so we will disable + // checkbox in filter scope selector. + // this function returns chart id based on current filter scope selector local state: + // 1. if in single-edit mode, return the chart id for selected filter field. + // 2. if in multi-edit mode, if all filter fields are from same chart id, + // return the single chart id. + // otherwise, there is no chart to disable. + if (activeFilterField) { + return getChartIdAndColumnFromFilterKey(activeFilterField).chartId; + } + + if (checkedFilterFields.length) { + const { chartId } = getChartIdAndColumnFromFilterKey( + checkedFilterFields[0], + ); + + if ( + checkedFilterFields.some( + filterKey => + getChartIdAndColumnFromFilterKey(filterKey).chartId !== chartId, + ) + ) { + return 0; + } + return chartId; + } + + return 0; +}