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

Batch actions and the new "shouldClearFilters" option #391

Merged
merged 21 commits into from
Sep 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 37 additions & 23 deletions ADVANCED.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/react-search-ui-views/src/SearchBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ function SearchBox(props) {
onSubmit,
useAutocomplete,
value,
// NOTE: These are explicitly de-structured but not used so that they are
// not passed through to the input with the 'rest' parameter
// eslint-disable-next-line no-unused-vars
autocompletedResults,
// eslint-disable-next-line no-unused-vars
Expand Down
18 changes: 13 additions & 5 deletions packages/react-search-ui/src/containers/SearchBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class SearchBoxContainer extends Component {
]),
autocompleteView: PropTypes.func,
className: PropTypes.string,
shouldClearFilters: PropTypes.bool,
debounceLength: PropTypes.number,
inputProps: PropTypes.object,
inputView: PropTypes.func,
Expand All @@ -51,7 +52,8 @@ export class SearchBoxContainer extends Component {
};

static defaultProps = {
autocompleteMinimumCharacters: 0
autocompleteMinimumCharacters: 0,
shouldClearFilters: true
};

state = {
Expand All @@ -71,22 +73,27 @@ export class SearchBoxContainer extends Component {
};

completeSuggestion = searchTerm => {
const { setSearchTerm } = this.props;
setSearchTerm(searchTerm);
const { shouldClearFilters, setSearchTerm } = this.props;
setSearchTerm(searchTerm, {
shouldClearFilters
});
};

handleSubmit = e => {
const { searchTerm, setSearchTerm } = this.props;
const { shouldClearFilters, searchTerm, setSearchTerm } = this.props;

e.preventDefault();
setSearchTerm(searchTerm);
setSearchTerm(searchTerm, {
shouldClearFilters
});
};

handleChange = value => {
const {
autocompleteMinimumCharacters,
autocompleteResults,
autocompleteSuggestions,
shouldClearFilters,
searchAsYouType,
setSearchTerm,
debounceLength
Expand All @@ -99,6 +106,7 @@ export class SearchBoxContainer extends Component {
searchAsYouType) && {
debounce: debounceLength || 200
}),
shouldClearFilters,
refresh: !!searchAsYouType,
autocompleteResults: !!autocompleteResults,
autocompleteSuggestions: !!autocompleteSuggestions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,65 @@ describe("useAutocomplete", () => {
});
});

describe("shouldClearFilters prop", () => {
it("will be passed through to setSearchTerm on submit", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
shouldClearFilters={false}
view={props => (viewProps = props)}
/>
);

const { onSubmit } = viewProps;
onSubmit({
preventDefault: () => {}
});
const call = params.setSearchTerm.mock.calls[0];
expect(call[1].shouldClearFilters).toEqual(false);
});

it("will be passed through to setSearchTerm on change", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
shouldClearFilters={false}
view={props => (viewProps = props)}
/>
);

const { onChange } = viewProps;
onChange("new term");
const call = params.setSearchTerm.mock.calls[0];
expect(call[1].shouldClearFilters).toEqual(false);
});

it("will call setSearchTerm if no onSelectAutocomplete is specified and a suggestion is selected", () => {
let viewProps;

shallow(
<SearchBoxContainer
{...params}
autocompleteResults={true}
shouldClearFilters={false}
view={props => (viewProps = props)}
/>
);

const { onSelectAutocomplete } = viewProps;
onSelectAutocomplete({
suggestion: "bird"
});

const call = params.setSearchTerm.mock.calls[0];
expect(call[1].shouldClearFilters).toEqual(false);
});
});

it("will call back to setSearchTerm with refresh: false when input is changed", () => {
let viewProps;
shallow(
Expand All @@ -220,6 +279,7 @@ it("will call back to setSearchTerm with refresh: false when input is changed",
refresh: false,
autocompleteResults: false,
autocompleteSuggestions: false,
shouldClearFilters: true,
autocompleteMinimumCharacters: 0
}
]);
Expand All @@ -243,6 +303,7 @@ it("will call back to setSearchTerm with autocompleteMinimumCharacters setting",
refresh: false,
autocompleteResults: false,
autocompleteSuggestions: false,
shouldClearFilters: true,
autocompleteMinimumCharacters: 3
}
]);
Expand Down Expand Up @@ -270,6 +331,7 @@ it("will call back to setSearchTerm with refresh: true when input is changed and
debounce: 200,
autocompleteResults: false,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteSuggestions: false
}
]);
Expand Down Expand Up @@ -298,6 +360,7 @@ it("will call back to setSearchTerm with a specific debounce when input is chang
debounce: 500,
autocompleteResults: false,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteSuggestions: false
}
]);
Expand Down Expand Up @@ -329,6 +392,7 @@ it("will call back to setSearchTerm with a specific debounce when input is chang
debounce: 500,
autocompleteResults: true,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteSuggestions: false
}
]);
Expand Down Expand Up @@ -357,6 +421,7 @@ it("will call back to setSearchTerm with a specific debounce when input is chang
debounce: 500,
autocompleteSuggestions: true,
autocompleteMinimumCharacters: 0,
shouldClearFilters: true,
autocompleteResults: false
}
]);
Expand All @@ -378,7 +443,7 @@ it("will call back setSearchTerm with refresh: true when form is submitted", ()
});

const call = params.setSearchTerm.mock.calls[0];
expect(call).toEqual(["a term"]);
expect(call).toEqual(["a term", { shouldClearFilters: true }]);
});

describe("onSelectAutocomplete", () => {
Expand Down Expand Up @@ -420,6 +485,25 @@ describe("onSelectAutocomplete", () => {
expect(passedAutocompleteResults).toBeDefined();
expect(passedDefaultOnSelectAutocomplete).toBeDefined();
});

it("will call setSearchTerm if no onSelectAutocomplete is specified and a suggestion is selected", () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
let viewProps;

shallow(
<SearchBoxContainer
{...params}
autocompleteResults={true}
view={props => (viewProps = props)}
/>
);
const { onSelectAutocomplete } = viewProps;
onSelectAutocomplete({
suggestion: "bird"
});

const call = params.setSearchTerm.mock.calls[0];
expect(call[0]).toEqual("bird");
});
});

describe("autocomplete clickthroughs", () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/search-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,10 @@
"deep-equal": "^1.0.1",
"history": "^4.9.0",
"qs": "^6.7.0"
},
"jest": {
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
]
}
}
74 changes: 60 additions & 14 deletions packages/search-ui/src/DebounceManager.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,76 @@
import debounceFn from "debounce-fn";

export default class DebounceManager {
class DebounceManager {
debounceCache = {};

/*
The purpose of this is to:
Dynamically debounce and cache a debounced version of a function at the time of calling that function. This avoids
managing debounced version of functions locally.

Assumption:
Functions are debounced on a combination of unique function and wait times. So debouncing won't work on
subsequent calls with different wait times or different functions. That also means that the debounce manager
can be used for different functions in parallel, and keep the two functions debounced separately.

*/
runWithDebounce(wait, fn, ...parameters) {
/**
* Dynamically debounce and cache a debounced version of a function at the time of calling that function. This avoids
* managing debounced version of functions locally.
*
* In other words, debounce usually works by debouncing based on
* referential identity of a function. This works by comparing provided function names.
*
* This also has the ability to short-circuit a debounce all-together, if no wait
* time is provided.
*
* Assumption:
* Functions are debounced on a combination of unique function name and wait times. So debouncing won't work on
* subsequent calls with different wait times or different functions. That also means that the debounce manager
* can be used for different functions in parallel, and keep the two functions debounced separately.
*
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
* @param {number} wait Milliseconds to debounce. Executes immediately if falsey.
* @param {function} fn Function to debounce
* @param {function} functionName Name of function to debounce, used to create a unique key
* @param {...any} parameters Parameters to pass to function
*/
runWithDebounce(wait, functionName, fn, ...parameters) {
if (!wait) {
return fn(...parameters);
}

const key = fn.toString() + wait.toString();
const key = `${functionName}|${wait.toString()}`;
let debounced = this.debounceCache[key];
if (!debounced) {
this.debounceCache[key] = debounceFn(fn, { wait });
debounced = this.debounceCache[key];
}
debounced(...parameters);
}

/**
* Cancels existing debounced function calls.
*
* This will cancel any debounced function call, regardless of the debounce length that was provided.
*
* For example, making the following series of calls will create multiple debounced functions, because
* they are cached by a combination of unique name and debounce length.
*
* runWithDebounce(1000, "_updateSearchResults", this._updateSearchResults)
* runWithDebounce(500, "_updateSearchResults", this._updateSearchResults)
* runWithDebounce(1000, "_updateSearchResults", this._updateSearchResults)
*
* Calling the following will cancel all of those, if they have not yet executed:
*
* cancelByName("_updateSearchResults")
*
* @param {string} functionName The name of the function that was debounced. This needs to match exactly what was provided
* when runWithDebounce was called originally.
*/
cancelByName(functionName) {
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
Object.entries(this.debounceCache)
.filter(([cachedKey]) => cachedKey.startsWith(`${functionName}|`))
// eslint-disable-next-line no-unused-vars
.forEach(([_, cachedValue]) => cachedValue.cancel());
}
}
/**
* Perform a standard debounce
*
* @param {number} wait Milliseconds to debounce. Executes immediately if falsey.
* @param {function} fn Function to debounce
*/
DebounceManager.debounce = (wait, fn) => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
return debounceFn(fn, { wait });
};

export default DebounceManager;