From 0854fb6d745dc7a89bf32a99021eaa9fae148e98 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Sat, 16 Feb 2019 14:12:10 +1100 Subject: [PATCH] Allow closing active story and use separate expansion for filtered For #5594 and #5419 --- .../src/components/sidebar/SidebarSearch.js | 6 +- .../components/sidebar/treeview/treeview.js | 147 ++++++++++++------ 2 files changed, 100 insertions(+), 53 deletions(-) diff --git a/lib/ui/src/components/sidebar/SidebarSearch.js b/lib/ui/src/components/sidebar/SidebarSearch.js index d55069863b0..3b8520420b4 100644 --- a/lib/ui/src/components/sidebar/SidebarSearch.js +++ b/lib/ui/src/components/sidebar/SidebarSearch.js @@ -98,8 +98,8 @@ const FilterForm = styled.form(({ theme, focussed }) => ({ }, })); -export const PureSidebarSearch = ({ focussed, onSetFocussed, className, ...props }) => ( - +export const PureSidebarSearch = ({ focussed, onSetFocussed, className, onReset, ...props }) => ( + { } }; -const calculateTreeProps = memoize(50)((input, filter, selectedId, extraExpanded) => { - const dataset = filter ? toFiltered(input, filter) : input; +const calculateTreeState = memoize(50)( + ({ dataset, selectedId }, { lastSelectedId, unfilteredExpanded }) => { + if (selectedId === lastSelectedId) { + return null; + } - const selected = Object.keys(dataset).reduce( - (acc, k) => Object.assign(acc, { [k]: k === selectedId }), - {} - ); + // If a new selection is made, we need to ensure it is part of the expanded set + const selectedAncestorIds = selectedId ? getParents(selectedId, dataset).map(i => i.id) : []; + + const newExpanded = Object.keys(dataset).reduce( + (acc, key) => ({ + ...acc, + [key]: selectedAncestorIds.includes(key) || unfilteredExpanded[key], + }), + {} + ); + + return { + lastSelectedId: selectedId, + unfilteredExpanded: newExpanded, + }; + } +); - const selectedAncestorIds = selectedId ? getParents(selectedId, dataset).map(i => i.id) : []; +const getExpanded = ({ unfilteredExpanded, filteredExpanded, filter }) => + filter ? filteredExpanded : unfilteredExpanded; - const expanded = Object.keys(dataset).reduce( - (acc, key) => ({ - ...acc, - [key]: selectedAncestorIds.includes(key) || extraExpanded[key], - }), +const getFilteredDataset = memoize(50)(({ dataset, filter }) => + filter ? toFiltered(dataset, filter) : dataset +); + +// Update the set of expansions we are currently working with +const updateExpanded = fn => ({ unfilteredExpanded, filteredExpanded, filter }) => { + if (filter) { + return { + filteredExpanded: fn(filteredExpanded), + }; + } + return { unfilteredExpanded: fn(unfilteredExpanded) }; +}; + +const getPropsForTree = memoize(50)(({ dataset, selectedId }) => { + const selected = Object.keys(dataset).reduce( + (acc, k) => Object.assign(acc, { [k]: k === selectedId }), {} ); @@ -166,25 +195,50 @@ const calculateTreeProps = memoize(50)((input, filter, selectedId, extraExpanded { roots: [], others: [] } ); - return { input, dataset, selected, expanded, roots, others }; + return { selected, roots, others }; }); // eslint-disable-next-line react/no-multi-comp class TreeState extends PureComponent { + state = { + // We maintain two sets of expanded nodes, so we remember which were expanded if we clear the filter + unfilteredExpanded: [], + filteredExpanded: [], + filter: null, + lastSelectedId: null, + }; + + static getDerivedStateFromProps(props, state) { + return calculateTreeState(props, state); + } + events = { onClick: (e, item) => { - const { extraExpanded } = this.state; + this.setState( + updateExpanded(expanded => ({ + ...expanded, + [item.id]: !expanded[item.id], + })) + ); + }, + onFilter: e => { + const { dataset } = this.props; + const filter = e.target.value && e.target.value.length >= 2 ? e.target.value : ''; + const filteredDataset = getFilteredDataset({ dataset, filter }); + + // Whenever we change the filter, we reset the "filtered" expanded set back to all matching stories this.setState({ - extraExpanded: { - ...extraExpanded, - [item.id]: !extraExpanded[item.id], - }, + filter, + filteredExpanded: + !!filter && + Object.keys(filteredDataset).reduce((acc, k) => Object.assign(acc, { [k]: true }), {}), }); }, onKeyUp: (e, item) => { - const { dataset: input, selectedId, prefix } = this.props; - const { filter, extraExpanded } = this.state; - const { dataset, expanded } = calculateTreeProps(input, filter, selectedId, extraExpanded); + const { prefix, dataset } = this.props; + const { filter } = this.state; + const filteredDataset = getFilteredDataset({ dataset, filter }); + const expanded = getExpanded(this.state); const action = keyEventToAction(e); if (action) { @@ -192,8 +246,8 @@ class TreeState extends PureComponent { } if (action === 'RIGHT') { - const next = getNext({ id: item.id, dataset, expanded }); - if (!dataset[item.id].children || expanded[item.id]) { + const next = getNext({ id: item.id, dataset: filteredDataset, expanded }); + if (!filteredDataset[item.id].children || expanded[item.id]) { if (next) { try { document.getElementById(createId(next.id, prefix)).focus(); @@ -203,13 +257,13 @@ class TreeState extends PureComponent { } } - this.setState({ extraExpanded: { ...extraExpanded, [item.id]: true } }); + this.setState(updateExpanded(currExpanded => ({ ...currExpanded, [item.id]: true }))); } if (action === 'LEFT') { - const prev = getPrevious({ id: item.id, dataset, expanded }); + const prev = getPrevious({ id: item.id, dataset: filteredDataset, expanded }); - if (!dataset[item.id].children || !expanded[item.id]) { - const parent = getParent(item.id, dataset); + if (!filteredDataset[item.id].children || !expanded[item.id]) { + const parent = getParent(item.id, filteredDataset); if (parent && parent.children) { try { document.getElementById(createId(parent.id, prefix)).focus(); @@ -227,10 +281,10 @@ class TreeState extends PureComponent { } } - this.setState({ extraExpanded: { ...extraExpanded, [item.id]: false } }); + this.setState(updateExpanded(currExpanded => ({ ...currExpanded, [item.id]: false }))); } if (action === 'DOWN') { - const next = getNext({ id: item.id, dataset, expanded }); + const next = getNext({ id: item.id, dataset: filteredDataset, expanded }); if (next) { try { document.getElementById(createId(next.id, prefix)).focus(); @@ -240,7 +294,7 @@ class TreeState extends PureComponent { } } if (action === 'UP') { - const prev = getPrevious({ id: item.id, dataset, expanded }); + const prev = getPrevious({ id: item.id, dataset: filteredDataset, expanded }); if (prev) { try { @@ -251,25 +305,15 @@ class TreeState extends PureComponent { } } }, - - onFilter: e => { - const filter = e.target.value.length >= 2 ? e.target.value : ''; - this.setState({ filter }); - }, - }; - - state = { - filter: null, - extraExpanded: {}, }; render() { const { events, - state: { filter, extraExpanded }, + state: { filter, unfilteredExpanded, filteredExpanded }, props, } = this; - const { prefix, dataset: input, selectedId } = props; + const { prefix, dataset, selectedId } = props; const Filter = getFilter(props.Filter); const List = getFilter(props.List); @@ -281,16 +325,15 @@ class TreeState extends PureComponent { const Section = getContainer(props.Section); const Message = getMessage(props.Message); - const { dataset, selected, expanded, roots, others } = calculateTreeProps( - input, - filter, - selectedId, - extraExpanded - ); + const filteredDataset = getFilteredDataset({ dataset, filter }); + const expanded = filter ? filteredExpanded : unfilteredExpanded; + const { selected, roots, others } = getPropsForTree({ dataset: filteredDataset, selectedId }); return ( - {Filter ? : null} + {Filter ? ( + + ) : null} {roots.length || others.length ? ( {roots.map(({ id, name, children }) => ( @@ -302,7 +345,7 @@ class TreeState extends PureComponent {