Skip to content

Commit 27d84fd

Browse files
authored
Merge pull request #737 from guardian/ljh-search-js-to-ts
Giant search: switch search.js and searchBox.js to typescript (no class-function conversion)
2 parents ca5950b + 37cad58 commit 27d84fd

7 files changed

Lines changed: 147 additions & 107 deletions

File tree

frontend/src/js/actions/urlParams/updateSearchQuery.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ export function updateSearchText(text: string): UrlParamsAction {
1717
};
1818
}
1919

20-
export function updatePage(page: number): UrlParamsAction {
20+
export function updatePage(page: string): UrlParamsAction {
2121
return {
2222
type: UrlParamsActionType.SEARCHQUERY_PAGE_UPDATE,
2323
page: page,
2424
};
2525
}
2626

27-
export function updatePageSize(pageSize: number): UrlParamsAction {
27+
export function updatePageSize(pageSize: string): UrlParamsAction {
2828
return {
2929
type: UrlParamsActionType.SEARCHQUERY_PAGE_SIZE_UPDATE,
3030
pageSize: pageSize,

frontend/src/js/components/Search/Search.js renamed to frontend/src/js/components/Search/Search.tsx

Lines changed: 112 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React from "react";
2-
import PropTypes from "prop-types";
32

43
import _isEqual from "lodash/fp/isEqual";
54
import SearchBox from "./SearchBox";
@@ -11,69 +10,81 @@ import { Checkbox } from "../UtilComponents/Checkbox";
1110
import { KeyboardShortcut } from "../UtilComponents/KeyboardShortcut";
1211
import Select from "react-select";
1312

14-
import { searchResultsPropType } from "../../types/SearchResults";
1513
import _get from "lodash/get";
1614
import _debounce from "lodash/debounce";
1715

18-
import { suggestedFieldsPropType } from "../../types/SuggestedFields";
1916
import { keyboardShortcuts } from "../../util/keyboardShortcuts";
2017
import SearchVisualizations from "./SearchVisualizations";
2118
import { calculateSearchTitle } from "../UtilComponents/documentTitle";
2219

2320
import { connect } from "react-redux";
2421
import { bindActionCreators } from "redux";
25-
26-
import { updateSearchText } from "../../actions/urlParams/updateSearchQuery";
22+
import { AnyAction } from "redux";
23+
import { ThunkDispatch } from "redux-thunk";
24+
25+
import { GiantState } from "../../types/redux/GiantState";
26+
import {
27+
updateSearchText,
28+
updateSearchQueryFilters,
29+
updatePage,
30+
updatePageSize,
31+
updateSortBy,
32+
} from "../../actions/urlParams/updateSearchQuery";
2733
import { performSearch } from "../../actions/search/performSearch";
2834
import { clearSearch } from "../../actions/search/clearSearch";
29-
import { updateSearchQueryFilters } from "../../actions/urlParams/updateSearchQuery";
30-
import { updatePage } from "../../actions/urlParams/updateSearchQuery";
31-
import { updatePageSize } from "../../actions/urlParams/updateSearchQuery";
32-
import { updateSortBy } from "../../actions/urlParams/updateSearchQuery";
3335
import { getSuggestedFields } from "../../actions/search/getSuggestedFields";
3436
import { resetResource } from "../../actions/resources/getResource";
3537
import { updatePreference } from "../../actions/preferences";
3638

37-
class Search extends React.Component {
38-
static propTypes = {
39-
urlParams: PropTypes.shape({
40-
q: PropTypes.string,
41-
page: PropTypes.any,
42-
pageSize: PropTypes.any,
43-
sortBy: PropTypes.string,
44-
filters: PropTypes.any,
45-
}),
46-
lastUri: PropTypes.string,
47-
updateSearchText: PropTypes.func.isRequired,
48-
updatePage: PropTypes.func.isRequired,
49-
updatePageSize: PropTypes.func.isRequired,
50-
updateSortBy: PropTypes.func.isRequired,
51-
performSearch: PropTypes.func.isRequired,
52-
clearSearch: PropTypes.func.isRequired,
53-
updateSearchQueryFilters: PropTypes.func.isRequired,
54-
resetResource: PropTypes.func.isRequired,
55-
getSuggestedFields: PropTypes.func.isRequired,
56-
updatePreference: PropTypes.func.isRequired,
57-
preferences: PropTypes.object,
58-
search: PropTypes.shape({
59-
isSearchInProgress: PropTypes.bool.isRequired,
60-
currentQuery: PropTypes.object,
61-
currentResults: searchResultsPropType,
62-
suggestedFields: PropTypes.arrayOf(suggestedFieldsPropType),
63-
searchFailed: PropTypes.bool,
64-
}).isRequired,
65-
};
39+
interface StateProps {
40+
urlParams: GiantState["urlParams"];
41+
search: GiantState["search"];
42+
lastUri: string | undefined;
43+
preferences: GiantState["app"]["preferences"];
44+
}
45+
46+
interface DispatchProps {
47+
getSuggestedFields: () => void;
48+
updateSearchText: (text: string) => void;
49+
updatePage: (page: string) => void;
50+
updatePageSize: (pageSize: string) => void;
51+
updateSortBy: (sortBy: string) => void;
52+
performSearch: (query: GiantState["urlParams"]) => void;
53+
clearSearch: () => void;
54+
updateSearchQueryFilters: (filters: object) => void;
55+
resetResource: () => void;
56+
updatePreference: (key: string, value: unknown) => void;
57+
}
58+
59+
type SearchProps = StateProps & DispatchProps;
60+
61+
interface SearchState {
62+
visibleText: string;
63+
}
64+
65+
interface SelectOption {
66+
value: string;
67+
label: string;
68+
}
6669

67-
state = {
70+
interface SearchBoxHandle {
71+
focus(): void;
72+
select(): void;
73+
}
74+
75+
class Search extends React.Component<SearchProps, SearchState> {
76+
searchBox: SearchBoxHandle | null = null;
77+
78+
state: SearchState = {
6879
visibleText: "",
6980
};
7081

71-
selectSearchBox = (e) => {
82+
selectSearchBox = (e: KeyboardEvent) => {
7283
e.preventDefault();
73-
this.searchBox.focus();
84+
this.searchBox?.focus();
7485
};
7586

76-
clearSearch = (e) => {
87+
clearSearch = (e: React.MouseEvent<HTMLButtonElement>) => {
7788
e.preventDefault();
7889

7990
this.updateVisibleText("");
@@ -82,25 +93,27 @@ class Search extends React.Component {
8293
this.props.updateSearchQueryFilters({});
8394
this.props.updatePage("1");
8495
this.setState({ visibleText: "" });
85-
this.searchBox.select();
96+
this.searchBox?.select();
8697
};
8798

88-
debouncedUpdate = _debounce((text) => {
99+
debouncedUpdate = _debounce((text: string) => {
89100
if (text !== this.props.urlParams.q) {
90101
this.props.updatePage("1");
91102
}
92103
this.props.updateSearchText(text);
93104

94-
this.triggerSearch(this.props.urlParams);
105+
// Use the freshly-typed text rather than this.props.urlParams.q, which
106+
// is the pre-dispatch value and would lag by one Enter press.
107+
this.triggerSearch({ ...this.props.urlParams, q: text });
95108
}, 500);
96109

97-
updateVisibleText = (text) => {
110+
updateVisibleText = (text: string) => {
98111
this.setState({
99112
visibleText: text,
100113
});
101114
};
102115

103-
triggerSearch(query) {
116+
triggerSearch(query: GiantState["urlParams"]) {
104117
if (query.q) {
105118
this.props.resetResource();
106119
this.props.performSearch(query);
@@ -131,7 +144,7 @@ class Search extends React.Component {
131144
document.title = calculateSearchTitle(this.props.search.currentQuery);
132145
}
133146

134-
UNSAFE_componentWillReceiveProps(props) {
147+
UNSAFE_componentWillReceiveProps(props: SearchProps) {
135148
const before = {
136149
filters: props.urlParams.filters,
137150
page: props.urlParams.page,
@@ -159,7 +172,7 @@ class Search extends React.Component {
159172
document.title = "Giant";
160173
}
161174

162-
pageSelectCallback = (page) => {
175+
pageSelectCallback = (page: number) => {
163176
this.props.updatePage(page.toString());
164177
};
165178

@@ -179,9 +192,33 @@ class Search extends React.Component {
179192

180193
renderControls() {
181194
// TODO replace with user preferences for page size
182-
const pageSize = this.props.urlParams.pageSize || "100";
195+
const pageSizeValue = this.props.urlParams.pageSize ?? "100";
183196
// TODO replace with user preferences for sort order
184-
const sortBy = this.props.urlParams.sortBy || "relevance";
197+
const sortByValue = this.props.urlParams.sortBy || "relevance";
198+
199+
const sortByOptions: SelectOption[] = [
200+
{ value: "relevance", label: "Sort by relevance" },
201+
{ value: "size-asc", label: "Sort by size (smallest first)" },
202+
{ value: "size-desc", label: "Sort by size (largest first)" },
203+
{
204+
value: "date-created-asc",
205+
label: "Sort by date created (oldest first)",
206+
},
207+
{
208+
value: "date-created-desc",
209+
label: "Sort by date created (newest first)",
210+
},
211+
];
212+
const pageSizeOptions: SelectOption[] = [
213+
{ value: "25", label: "25 results per page" },
214+
{ value: "50", label: "50 results per page" },
215+
{ value: "100", label: "100 results per page" },
216+
];
217+
218+
const currentSortBy = sortByOptions.find((o) => o.value === sortByValue);
219+
const currentPageSize = pageSizeOptions.find(
220+
(o) => o.value === pageSizeValue,
221+
);
185222

186223
return (
187224
<div className="search__controls">
@@ -199,37 +236,25 @@ class Search extends React.Component {
199236
</Checkbox>
200237
<Select
201238
className="search__control"
202-
value={sortBy}
203-
options={[
204-
{ value: "relevance", label: "Sort by relevance" },
205-
{ value: "size-asc", label: "Sort by size (smallest first)" },
206-
{ value: "size-desc", label: "Sort by size (largest first)" },
207-
{
208-
value: "date-created-asc",
209-
label: "Sort by date created (oldest first)",
210-
},
211-
{
212-
value: "date-created-desc",
213-
label: "Sort by date created (newest first)",
214-
},
215-
]}
239+
value={currentSortBy}
240+
options={sortByOptions}
216241
onChange={(v) => {
242+
const option = v as SelectOption | null;
243+
if (!option) return;
217244
this.props.updatePage("1");
218-
this.props.updateSortBy(v.value);
245+
this.props.updateSortBy(option.value);
219246
}}
220247
clearable={false}
221248
/>
222249
<Select
223250
className="search__control"
224-
value={pageSize}
225-
options={[
226-
{ value: "25", label: "25 results per page" },
227-
{ value: "50", label: "50 results per page" },
228-
{ value: "100", label: "100 results per page" },
229-
]}
251+
value={currentPageSize}
252+
options={pageSizeOptions}
230253
onChange={(v) => {
254+
const option = v as SelectOption | null;
255+
if (!option) return;
231256
this.props.updatePage("1");
232-
this.props.updatePageSize(v.value);
257+
this.props.updatePageSize(option.value);
233258
}}
234259
clearable={false}
235260
/>
@@ -267,10 +292,9 @@ class Search extends React.Component {
267292
func={this.selectSearchBox}
268293
/>
269294
<SearchBox
270-
ref={(input) => (this.searchBox = input)}
295+
ref={(input: SearchBoxHandle | null) => (this.searchBox = input)}
271296
updateVisibleText={this.updateVisibleText}
272297
resetQuery={this.clearSearch}
273-
addQuery={this.addQuery}
274298
q={this.state.visibleText}
275299
isSearchInProgress={this.props.search.isSearchInProgress}
276300
suggestedFields={this.props.search.suggestedFields}
@@ -308,7 +332,7 @@ class Search extends React.Component {
308332
}
309333
}
310334

311-
function mapStateToProps(state) {
335+
function mapStateToProps(state: GiantState): StateProps {
312336
return {
313337
urlParams: state.urlParams,
314338
search: state.search,
@@ -317,7 +341,9 @@ function mapStateToProps(state) {
317341
};
318342
}
319343

320-
function mapDispatchToProps(dispatch) {
344+
function mapDispatchToProps(
345+
dispatch: ThunkDispatch<GiantState, undefined, AnyAction>,
346+
): DispatchProps {
321347
return {
322348
getSuggestedFields: bindActionCreators(getSuggestedFields, dispatch),
323349
updateSearchText: bindActionCreators(updateSearchText, dispatch),
@@ -335,4 +361,12 @@ function mapDispatchToProps(dispatch) {
335361
};
336362
}
337363

338-
export default connect(mapStateToProps, mapDispatchToProps)(Search);
364+
export default connect<
365+
StateProps,
366+
DispatchProps,
367+
Record<string, never>,
368+
GiantState
369+
>(
370+
mapStateToProps,
371+
mapDispatchToProps,
372+
)(Search);

frontend/src/js/components/Search/SearchBox.js renamed to frontend/src/js/components/Search/SearchBox.tsx

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,32 @@
11
import React from "react";
2-
import PropTypes from "prop-types";
32

43
import { ProgressAnimation } from "../UtilComponents/ProgressAnimation";
5-
import { suggestedFieldsPropType } from "../../types/SuggestedFields";
6-
74
import InputSupper from "../UtilComponents/InputSupper";
5+
import { SuggestedField } from "../../types/SuggestedFields";
86

9-
export default class SearchBox extends React.Component {
10-
static propTypes = {
11-
q: PropTypes.string.isRequired,
12-
isSearchInProgress: PropTypes.bool.isRequired,
13-
updateVisibleText: PropTypes.func.isRequired,
14-
resetQuery: PropTypes.func.isRequired,
15-
suggestedFields: PropTypes.arrayOf(suggestedFieldsPropType),
16-
updateSearchText: PropTypes.func.isRequired,
17-
};
7+
interface InputSupperHandle {
8+
focus(): void;
9+
select(): void;
10+
}
11+
12+
export interface SearchBoxProps {
13+
q: string;
14+
isSearchInProgress: boolean;
15+
updateVisibleText: (text: string) => void;
16+
resetQuery: (e: React.MouseEvent<HTMLButtonElement>) => void;
17+
suggestedFields?: SuggestedField[];
18+
updateSearchText: () => void;
19+
}
20+
21+
export default class SearchBox extends React.Component<SearchBoxProps> {
22+
searchInput: InputSupperHandle | null = null;
1823

1924
focus = () => {
20-
if (this.searchInput !== document.activeElement) {
21-
this.searchInput.focus();
22-
// If you previously did select and didn't type anything the input box 'remembers' it was selected
23-
// so we need this hack to reset that...
24-
const value = this.searchInput.value;
25-
this.searchInput.value = value;
26-
}
25+
this.searchInput?.focus();
2726
};
2827

2928
select = () => {
30-
if (this.searchInput !== document.activeElement) {
31-
this.searchInput.select();
32-
}
29+
this.searchInput?.select();
3330
};
3431

3532
render() {
@@ -42,7 +39,7 @@ export default class SearchBox extends React.Component {
4239
<div>
4340
<div className="search-box">
4441
<InputSupper
45-
ref={(s) => (this.searchInput = s)}
42+
ref={(s: InputSupperHandle | null) => (this.searchInput = s)}
4643
className="search-box__input"
4744
value={this.props.q}
4845
chips={this.props.suggestedFields}

0 commit comments

Comments
 (0)