Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search: Add sort order to search results #1726

Closed
wants to merge 10 commits into from

Conversation

codebykat
Copy link
Member

@codebykat codebykat commented Nov 16, 2019

Enhancement

This PR adds an ordering toggle to the bottom of the notes list when a search is active:

Screen Shot 2019-11-27 at 10 38 23 PM

Will fix #1625

Test

  1. Enter a search
  2. Use the toggle to change the sort type and direction of the results.
  3. Clear the search and verify that your global sort settings haven't been changed.
  4. Start a new search and note that the sorting resets to the global setting.
  5. Try in different browsers/OSes to make sure the dropdown styling is okay

NOTE: Title matches will get sorted to the top of the list as per #1705. So there's basically two separate sections: title and content. Within each section the notes should get sorted as per preferences. It's out of scope but I'd like to make this clearer in a separate PR.

Dark Mode screenshots

Screen Shot 2019-11-27 at 10 37 51 PM

Screen Shot 2019-11-27 at 10 37 56 PM

Release

RELEASE-NOTES.txt was updated in
932676d with:

Added sort order to search results #1726

@codebykat codebykat added this to the Advanced Search milestone Nov 16, 2019
@codebykat codebykat self-assigned this Nov 16, 2019
@codebykat codebykat force-pushed the try/sort-order-in-notes-list branch from 65a002d to a7631d8 Compare November 27, 2019 07:27
@codebykat codebykat force-pushed the try/sort-order-in-notes-list branch from a7631d8 to 09c9cb3 Compare November 27, 2019 07:28
@codebykat codebykat changed the title [WIP] Search: Add sort order to search results Search: Add sort order to search results Nov 28, 2019
@codebykat codebykat marked this pull request as ready for review November 28, 2019 06:45
Copy link
Member

@dmsnell dmsnell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codebykat this is coming along and it looks like quite the challenge to figure out the details and ways that things are intertwined. good job and good luck on this journey!

});

function mapDispatchToProps(dispatch, { noteBucket }) {
const actionCreators = Object.assign({}, appState.actionCreators);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn’t be necessary. Why is it here?

const thenReloadNotes = action => a => {
dispatch(action(a));
dispatch(actionCreators.loadNotes({ noteBucket }));
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With two invocations right next to each other this function may not worth the extra abstraction it brings.

@@ -52,6 +52,18 @@ export const setSortType = sortType => ({
sortType,
});

export const setSearchSortType = searchSortType => ({
type: 'setSearchSortType',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add new constants in the action-types file and follow the direction of Calypso? Why? The list of named action types gives us one place to see all the kinds of things we can do in the app and helps prevent us from creating duplicate actions; it makes it easier to analyze the code programmatically; we get free compiler errors if we try to use an action type that doesn't exist because webpack will complain when we import { NONEXISTENT_TYPE } from '../action-types'

dispatch({
type: 'setSearchSortReversed',
searchSortReversed: !getState().settings.searchSortReversed,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's a philosophical point of reflection: where is best to store the "toggle" state?

we can do it here in Redux middleware, which makes the UI components easier because they only need to call toggle(), but it complicates the Redux flows because the actions are no longer predictable.

we can do it in the UI and pass the next state, but that means we have to track it in the UI component and so complicates/duplicates it there.

one place I've come to accept is to use a hybrid of mapStateToProps() to get the existing state, mapDispatchToProps() to get the dispatch function, and occasionally, rarely, to use mergeProps() (also part of connect() but usually one we leave out) to keep the UI API clean and agnostic about the toggle state.


usually these complications are indications of a missing Eureka moment in a data structure somewhere. why is it hard for the UI to send the toggle state? (hint, boolean values should always raise our suspicion)


🤔 what if we stored sortDirection instead of isReversed?

export const toggleSearchDirectionFrom = direction => ( {
  type: SEARCH_SET_DIRECTION,
  direction: direction === 'normal' ? 'reversed' : 'normal',
} )

Manage it in the UI

not so bad because the state is simple enough and clear enough

toggleSearchDirection = () => this.props.toggleSearchDirection( this.props.searchDirection )

<button onClick={ this.toggleSearchDirection } />

const mapStateToProps = ( { search: { direction } } ) => ( {
  searchDirection: direction,
} )

const mapDispatchToProps = dispatch => ( {
  toggleSearchDirection: direction => dispatch( toggleSearchDirectionFrom( direction ) ),
} )

use mergeProps to remove the knowledge from the UI

more complicated connect() but more straightforward UI

<button onClick={ this.props.toggleSearchDirection } />

const mapStateToProps = ( { search: { direction } } ) => ( {
  searchDirection: direction,
} )

const mapDispatchToProps = dispatch => ( {
  setSearchDirection: direction => dispatch( toggleSearchDirectionFrom( direction ) ),
} )

const mergeProps = ( stateProps, dispatchProps, ownProps ) => ( {
  ...ownProps,
  ...stateProps,
  ...dispatchProps,
  toggleSearchDirection: () => dispatchProps.toggleSearchDirection( ownProps.searchDirection ),
} )

connect( mapStateToProps, mapDispatchToProps, mergeProps )( Component );

Of course, that looks scary, especially if unfamiliar with it. I'm a fan of tracking this in the UI until it's clear we need more abstraction. In any case, I still prefer keeping middleware behaviors outside of the action creators because of the pain I've had in trying to debug redux thunk stuff like this. In #1725 I recently proposed a new Redux middleware just for the purpose of executing the current search query (which is of course a bit more complicated because the search params are complicated). It's not that much more than Redux thunk actions but it preserves the predictability and audibility of our action sequence. [adding middleware, defining middleware]

Middleware for predictability and introspection

export const toggleSortOrder = () => ( {
  type: SORT_ORDER_TOGGLE,
} );

export const sortDirection = ( state = 'normal', { type, direction } ) =>
  SORT_ORDER_SET === 

Eureka!

See? it was way simpler than we were realizing. This is best handled in the reducer where all the data lives.

export const sortOrder = ( state = 'normal', { type } ) =>
  SORT_ORDER_TOGGLE === type
    ? ( state === 'normal' ? 'reverse' : 'normal' )
    : state;

export toggleSortOrder = () => ( {
  type: SORT_ORDER_TOGGLE,
} );

<button onClick={ this.props.toggleSortOrder } />

const mapStateToProps = ( { search: { sortOrder } } ) => ( {
  sortOrder,
} )

const mapDispatchToProps = ( {
  toggleSortOrder
} )

case 'setSearchSortReversed':
return { ...state, searchSortReversed: action.searchSortReversed };
case 'setSearchSortType':
return { ...state, searchSortType: action.searchSortType };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would have good value in using a new reducer in a search directory where we can store all the filtering and sort properties. these reducers used to be separate but they were merged and that can cause issues, especially with regards to handling more than one action type in a given reducer. it might be worth our time to decouple these as they once were but that would be a job for another PR

you can see what I was exploring in https://github.com/Automattic/simplenote-electron/pull/1725/files#diff-a36f9dcd572ee4de0417fd7a1f181542

@SylvesterWilmott
Copy link
Contributor

Looks good!

We can an icon we can use for the reordering results in the new icon pack

const { sortType, sortReversed, searchSortReversed, query } = this.props;
/* If there's a query and there wasn't before
(i.e., the search sort component is being newly displayed),
set the search sort to match the global setting.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have local and global search orders? Would it make sense to only have a single global sort order that changes when we click to change it?

That would simplify the amount of code and complexity we have to introduce to support both sorting systems


return (
<Fragment>
{query.length > 0 && (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we leave this check for the calling parent component then it will clean up this function and we can remove the <Fragment>

It means moving the check to the parent component but at least then what it leaves us is a component that can stand on its own a bit better.

@codebykat codebykat modified the milestones: Advanced Search, 2.5.0 Dec 23, 2020
@codebykat
Copy link
Member Author

Closing in favor of #2542

@codebykat codebykat closed this Dec 24, 2020
@codebykat codebykat deleted the try/sort-order-in-notes-list branch December 24, 2020 05:55
@codebykat codebykat removed this from the 2.5.0 milestone Jan 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Search: Add "sort by" to search results
4 participants