Skip to content

Commit 7b64e6c

Browse files
authored
feat: make redirects via the input component case-insensitive (#879)
Revert "Revert "feat: make redirects via the input component case-insensitive" (#877)" This reverts commit f2279a4. Take 2 of #875. DAU-249
1 parent 210c1a8 commit 7b64e6c

File tree

7 files changed

+91
-9
lines changed

7 files changed

+91
-9
lines changed

.changeset/tough-rabbits-fold.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@sajari/react-search-ui': major
3+
'sajari-sdk-docs': patch
4+
---
5+
6+
feat: make redirects via the input component case-insensitive
7+
8+
Note: This update includes a breaking change to the way redirects work in the `@sajari/react-search-ui` `Input` component. The component no longer supports case-sensitive redirects.

.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"react/react-in-jsx-scope": "off",
1414

1515
// TODO: We should re-enable this if we think it's worthwhile
16-
"@typescript-eslint/explicit-module-boundary-types": "off"
16+
"@typescript-eslint/explicit-module-boundary-types": "off",
17+
"no-plusplus": "off"
1718
},
1819
"settings": {
1920
"react": {

docs/pages/search-ui/input.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ function Example() {
133133
}
134134
```
135135
136+
### Note
137+
138+
The `Input` component treats redirects as _case-insensitive_, meaning the user-inputted value `Computer` returns the redirect for the query `computer` and vice versa.
139+
140+
The component does not support the handling of multiple redirects for queries of the same word with varied letter casing. For example, if you have created redirects for each of the queries `computer`, `Computer`, and `COMPUTER`, only one of them is used (dependent upon your browser).
141+
136142
## Results
137143
138144
```jsx

packages/search-ui/src/Input/index.test.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const redirectTarget = {
1616
token:
1717
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoic2VhcmNoIiwiZGVzdGluYXRpb24iOiJodHRwOi8vdGFyZ2V0LmNvbS5hdS9zaGVldHMiLCJ2YWxzIjp7ImNvbGxlY3Rpb24iOlsiYmVzdGJ1eSJdLCJpZGVudGlmaWVyIjpbInJlZGlyZWN0Il0sInByb2plY3QiOlsiMTU5NDE1MzcxMTkwMTcyNDIyMCJdLCJxIjpbInNoZWV0cyJdLCJxLmlkIjpbIjc2ZGJiOGU2LWE3MDctNDU2NC1iYTYxLWY0NjNiYTRhZDdlYSJdLCJxLnVpZCI6WyI3NmRiYjhlNi1hNzA3LTQ1NjQtYmE2MS1mNDYzYmE0YWQ3ZWEwIl0sInJlZGlyZWN0LkNvbmRpdGlvbiI6WyJxIH4gJ3NoZWV0cyciXSwicmVkaXJlY3QuSUQiOlsiMjJ3MFZGdkdWYVlhQ1Jzc0NBUm11YkQ2bGdUIl0sInJlZGlyZWN0LlRhcmdldCI6WyJodHRwOi8vdGFyZ2V0LmNvbS5hdS9zaGVldHMiXX19.BhcAVPB4z9LjlIoV42CUaEW-H0qCJ2JKngs6OGAXTf8',
1818
};
19+
const redirectTarget2 = {
20+
id: '12w0VFvGVaYaCRssCARmubD6lgA',
21+
target: 'http://target.com.au/sheets2',
22+
token:
23+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdXJwb3NlIjoic2VhcmNoIiwiZGVzdGluYXRpb24iOiJodHRwOi8vdGFyZ2V0LmNvbS5hdS9zaGVldHMiLCJ2YWxzIjp7ImNvbGxlY3Rpb24iOlsiYmVzdGJ1eSJdLCJpZGVudGlmaWVyIjpbInJlZGlyZWN0Il0sInByb2plY3QiOlsiMTU5NDE1MzcxMTkwMTcyNDIyMCJdLCJxIjpbInNoZWV0cyJdLCJxLmlkIjpbIjc2ZGJiOGU2LWE3MDctNDU2NC1iYTYxLWY0NjNiYTRhZDdlYSJdLCJxLnVpZCI6WyI3NmRiYjhlNi1hNzA3LTQ1NjQtYmE2MS1mNDYzYmE0YWQ3ZWEwIl0sInJlZGlyZWN0LkNvbmRpdGlvbiI6WyJxIH4gJ3NoZWV0cyciXSwicmVkaXJlY3QuSUQiOlsiMjJ3MFZGdkdWYVlhQ1Jzc0NBUm11YkQ2bGdUIl0sInJlZGlyZWN0LlRhcmdldCI6WyJodHRwOi8vdGFyZ2V0LmNvbS5hdS9zaGVldHMiXX19.BhcAVPB4z9LjlIoV42CUaEW-H0qCJ2JKngs6OGAXTf9',
24+
};
1925
const iPhoneResult = {
2026
values: {
2127
_id: {
@@ -49,10 +55,11 @@ const server = setupServer(
4955
values: {
5056
q: 'sheets',
5157
'q.original': 'sheets',
52-
'q.suggestions': 'sheets',
58+
'q.suggestions': 'suggestion',
5359
},
5460
redirects: {
5561
sheets: redirectTarget,
62+
Sheets: redirectTarget2,
5663
},
5764
searchResponse: {
5865
reads: '141',
@@ -77,6 +84,7 @@ describe('Input', () => {
7784
});
7885
afterEach(() => {
7986
jest.clearAllMocks();
87+
jest.resetAllMocks();
8088
server.resetHandlers();
8189
});
8290
afterAll(() => {
@@ -92,12 +100,12 @@ describe('Input', () => {
92100
const input = screen.getByTestId('mysearch');
93101
input.focus(); // need this else we get TypeError: element.ownerDocument.getSelection is not a function
94102
await user.keyboard('sheets');
95-
await waitFor(() => expect(screen.getByText('sheets')).toBeInTheDocument());
103+
await waitFor(() => expect(screen.getByRole('option')).toHaveTextContent('suggestion'));
96104
await user.keyboard('{enter}');
97105

98106
expect(onRedirectSpy).toHaveBeenCalledWith({
99-
...redirectTarget,
100-
token: `https://re.sajari.com/token/${redirectTarget.token}`, // sdk-js prepends the clickTokenURL
107+
...redirectTarget2,
108+
token: `https://re.sajari.com/token/${redirectTarget2.token}`, // sdk-js appends the clickTokenURL
101109
});
102110
});
103111

@@ -156,3 +164,31 @@ describe('Input', () => {
156164
await waitFor(() => expect(input.attributes.getNamedItem('readonly')).toBeNull());
157165
});
158166
});
167+
168+
describe('redirects', () => {
169+
beforeAll(() => {
170+
server.listen({ onUnhandledRequest: 'warn' });
171+
});
172+
afterAll(() => {
173+
jest.restoreAllMocks();
174+
server.close();
175+
});
176+
177+
it('ignores letter casing of input query on redirect lookup', async () => {
178+
const onRedirectSpy = jest.spyOn(eventTrackingPipeline.getTracking(), 'onRedirect');
179+
customRender(<Input data-testid="mysearch" mode="suggestions" />, {
180+
search: { pipeline: eventTrackingPipeline },
181+
});
182+
const input = screen.getByTestId('mysearch');
183+
input.focus(); // need this else we get TypeError: element.ownerDocument.getSelection is not a function
184+
await userEvent.type(input, 'ShEeTs');
185+
await waitFor(() => expect(screen.getByRole('option')).toHaveTextContent('suggestion'));
186+
await userEvent.keyboard('{enter}');
187+
await waitFor(() => {
188+
expect(onRedirectSpy).toHaveBeenCalledWith({
189+
...redirectTarget2,
190+
token: `https://re.sajari.com/token/${redirectTarget2.token}`, // sdk-js appends the clickTokenURL
191+
});
192+
});
193+
});
194+
});

packages/search-ui/src/Input/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useSearchUIContext } from '../ContextProvider';
1010
import { ResultValues } from '../Results/types';
1111
import mapResultFields from '../utils/mapResultFields';
1212
import { InputProps } from './types';
13+
import { lowercaseObjectKeys } from './utils';
1314

1415
const Input = React.forwardRef((props: InputProps<any>, ref: React.ForwardedRef<HTMLInputElement>) => {
1516
const { t } = useTranslation('input');
@@ -36,8 +37,9 @@ const Input = React.forwardRef((props: InputProps<any>, ref: React.ForwardedRef<
3637
redirects,
3738
searching: autocompleteSearching,
3839
} = useAutocomplete();
39-
const redirectsRef = useRef(redirects);
40-
redirectsRef.current = redirects;
40+
const lowercasedRedirects = lowercaseObjectKeys(redirects);
41+
const redirectsRef = useRef(lowercasedRedirects);
42+
redirectsRef.current = lowercasedRedirects;
4143
const { customClassNames, disableDefaultStyles = false, tracking } = useSearchUIContext();
4244
const { query } = useQuery();
4345
const [internalSuggestions, setInternalSuggestions] = useState<Array<any>>([]);
@@ -174,7 +176,7 @@ const Input = React.forwardRef((props: InputProps<any>, ref: React.ForwardedRef<
174176
if (!retainFilters) {
175177
resetFilters();
176178
}
177-
const redirectValue = redirectsRef.current[value];
179+
const redirectValue = redirectsRef.current[value.toLowerCase()];
178180
if (!disableRedirects && redirectValue) {
179181
tracking.onRedirect(redirectValue);
180182
window.location.assign(redirectValue.token || redirectValue.target);
@@ -183,7 +185,7 @@ const Input = React.forwardRef((props: InputProps<any>, ref: React.ForwardedRef<
183185
// If we're performing an autocomplete search, wait a tick to recheck redirects before unloading
184186
e.preventDefault();
185187
setTimeout(() => {
186-
const redirectTarget = redirectsRef.current[value];
188+
const redirectTarget = redirectsRef.current[value.toLowerCase()];
187189
if (redirectTarget) {
188190
tracking.onRedirect(redirectTarget);
189191
window.location.assign(redirectTarget.token || redirectTarget.target);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { lowercaseObjectKeys } from '.';
2+
3+
test('lowercaseRedirects', () => {
4+
const redirects = {
5+
FoO: { id: '1', target: 'a' },
6+
BAR: { id: '2', target: 'b' },
7+
baz: { id: '3', target: 'c' },
8+
test: { id: '4', target: 'd' },
9+
Test: { id: '5', target: 'e' },
10+
};
11+
const want = {
12+
foo: { id: '1', target: 'a' },
13+
bar: { id: '2', target: 'b' },
14+
baz: { id: '3', target: 'c' },
15+
test: { id: '5', target: 'e' },
16+
};
17+
18+
const got = lowercaseObjectKeys(redirects);
19+
expect(got).toStrictEqual(want);
20+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function lowercaseObjectKeys(obj: object) {
2+
const newObj = {};
3+
const keys = Object.keys(obj);
4+
for (let i = 0; i < keys.length; i++) {
5+
const key = keys[i];
6+
newObj[key.toLowerCase()] = obj[key];
7+
}
8+
return newObj;
9+
}

0 commit comments

Comments
 (0)