From f0c8cf0e79f003514fd65a70def5820205955a77 Mon Sep 17 00:00:00 2001 From: Noah Loomans Date: Thu, 21 Dec 2017 12:06:41 +0100 Subject: Move to typescript --- src/client/react/reducers/search.js | 81 ------------- src/client/react/reducers/search.test.js | 193 ------------------------------- src/client/react/reducers/search.test.ts | 193 +++++++++++++++++++++++++++++++ src/client/react/reducers/search.ts | 90 ++++++++++++++ 4 files changed, 283 insertions(+), 274 deletions(-) delete mode 100644 src/client/react/reducers/search.js delete mode 100644 src/client/react/reducers/search.test.js create mode 100644 src/client/react/reducers/search.test.ts create mode 100644 src/client/react/reducers/search.ts (limited to 'src/client/react/reducers') diff --git a/src/client/react/reducers/search.js b/src/client/react/reducers/search.js deleted file mode 100644 index 2a7e7a5..0000000 --- a/src/client/react/reducers/search.js +++ /dev/null @@ -1,81 +0,0 @@ -import fuzzy from 'fuzzy'; -import users from '../users'; - -const DEFAULT_STATE = { - results: [ - 's/18562', - ], - selectedResult: null, - isExactMatch: false, -}; - -function getSearchResults(allUsers, query) { - if (query.trim() === '') { - return []; - } - - const allResults = fuzzy.filter(query, allUsers, { - extract: user => user.value, - }); - - const firstResults = allResults.splice(0, 4); - const userIds = firstResults.map(result => result.original.id); - - return userIds; -} - -const search = (state = DEFAULT_STATE, action) => { - switch (action.type) { - case 'SEARCH/INPUT_CHANGE': { - const results = getSearchResults(users.allUsers, action.typedValue); - let selectedResult = null; - let isExactMatch = false; - - // Is the typed value exactly the same as the first result? Then show the - // appropiate icon instead of the generic search icon. - if ((results.length === 1) && (action.typedValue === users.byId[results[0]].value)) { - [selectedResult] = results; - isExactMatch = true; - } - - return { - ...state, - results, - selectedResult, - isExactMatch, - }; - } - - case 'SEARCH/CHANGE_SELECTED_RESULT': { - const { results, isExactMatch } = state; - - if (isExactMatch) return state; - - const prevSelectedResult = state.selectedResult; - const prevSelectedResultIndex = results.indexOf(prevSelectedResult); - let nextSelectedResultIndex = - prevSelectedResultIndex + action.relativeChange; - - if (nextSelectedResultIndex < -1) { - nextSelectedResultIndex = results.length - 1; - } else if (nextSelectedResultIndex > results.length - 1) { - nextSelectedResultIndex = -1; - } - - const nextSelectedResult = - nextSelectedResultIndex === -1 - ? null - : results[nextSelectedResultIndex]; - - return { - ...state, - selectedResult: nextSelectedResult, - }; - } - - default: - return state; - } -}; - -export default search; diff --git a/src/client/react/reducers/search.test.js b/src/client/react/reducers/search.test.js deleted file mode 100644 index e0ca18e..0000000 --- a/src/client/react/reducers/search.test.js +++ /dev/null @@ -1,193 +0,0 @@ -window.USERS = [ - { type: 's', value: '18561' }, - { type: 's', value: '18562' }, - { type: 's', value: '18563' }, - { type: 's', value: '18564' }, - { type: 's', value: '18565' }, - { type: 's', value: '18566' }, - { type: 's', value: '18567' }, - { type: 's', value: '18568' }, - { type: 's', value: '18569' }, -]; - -const deepFreeze = require('deep-freeze'); -const search = require('./search').default; -const { inputChange, changeSelectedResult } = require('../actions/search'); - -describe('reducers', () => { - describe('search', () => { - describe('SEARCH/INPUT_CHANGE', () => { - it('Returns no results when nothing is typed in', () => { - expect(search(undefined, inputChange(''))).toEqual({ - results: [], - selectedResult: null, - isExactMatch: false, - }); - }); - - it('Returns no results when a space is typed in', () => { - expect(search(undefined, inputChange(' '))).toEqual({ - results: [], - selectedResult: null, - isExactMatch: false, - }); - }); - - it('Preforms a basic search, only returning four results', () => { - expect(search(undefined, inputChange('18'))).toEqual({ - results: [ - 's/18561', - 's/18562', - 's/18563', - 's/18564', - ], - selectedResult: null, - isExactMatch: false, - }); - }); - - it('Selects the first result and sets isExactMatch to true when there is an exact match', () => { - expect(search(undefined, inputChange('18561'))).toEqual({ - results: [ - 's/18561', - ], - selectedResult: 's/18561', - isExactMatch: true, - }); - }); - }); - - describe('SEARCH/CHANGE_SELECTED_RESULT', () => { - it('Does nothing when there are no results', () => { - const prevState = { - results: [], - selectedResult: null, - isExactMatch: false, - }; - - const actionPlus = changeSelectedResult(+1); - const actionMin = changeSelectedResult(-1); - - deepFreeze([prevState, actionPlus, actionMin]); - - const nextStatePlus = search(prevState, actionPlus); - const nextStateMin = search(prevState, actionMin); - expect(nextStatePlus).toEqual(prevState); - expect(nextStateMin).toEqual(prevState); - }); - - it('Does nothing when there is an exact match', () => { - const prevState = { - results: ['s/18561'], - selectedResult: 's/18561', - isExactMatch: true, - }; - - const actionPlus = changeSelectedResult(+1); - const actionMin = changeSelectedResult(-1); - - deepFreeze([prevState, actionPlus, actionMin]); - - const nextStatePlus = search(prevState, actionPlus); - const nextStateMin = search(prevState, actionMin); - - expect(nextStatePlus).toEqual(prevState); - expect(nextStateMin).toEqual(prevState); - }); - - it('Switches to the correct selectedResult when no selected result is selected', () => { - const prevState = { - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: null, - isExactMatch: false, - }; - - const actionPlus = changeSelectedResult(+1); - const actionMin = changeSelectedResult(-1); - - deepFreeze([prevState, actionPlus, actionMin]); - - const nextStatePlus = search(prevState, actionPlus); - const nextStateMin = search(prevState, actionMin); - - expect(nextStatePlus).toEqual({ - ...prevState, - selectedResult: 's/18561', - }); - expect(nextStateMin).toEqual({ - ...prevState, - selectedResult: 's/18563', - }); - }); - - it('Switches to the correct selectedResult when there is a selected result selected', () => { - const prevState = { - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: 's/18562', - isExactMatch: false, - }; - - const actionPlus = changeSelectedResult(+1); - const actionMin = changeSelectedResult(-1); - - deepFreeze([prevState, actionPlus, actionMin]); - - const nextStatePlus = search(prevState, actionPlus); - const nextStateMin = search(prevState, actionMin); - - expect(nextStatePlus).toEqual({ - ...prevState, - selectedResult: 's/18563', - }); - expect(nextStateMin).toEqual({ - ...prevState, - selectedResult: 's/18561', - }); - }); - - it('Properly wraps arround when incrementing', () => { - expect(search({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: 's/18563', - isExactMatch: false, - }, changeSelectedResult(+1))).toEqual({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: null, - isExactMatch: false, - }); - - expect(search({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: null, - isExactMatch: false, - }, changeSelectedResult(+1))).toEqual({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: 's/18561', - isExactMatch: false, - }); - }); - - it('Properly wraps arround when decrementing', () => { - expect(search({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: 's/18561', - isExactMatch: false, - }, changeSelectedResult(-1))).toEqual({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: null, - isExactMatch: false, - }); - - expect(search({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: null, - isExactMatch: false, - }, changeSelectedResult(-1))).toEqual({ - results: ['s/18561', 's/18562', 's/18563'], - selectedResult: 's/18563', - isExactMatch: false, - }); - }); - }); - }); -}); diff --git a/src/client/react/reducers/search.test.ts b/src/client/react/reducers/search.test.ts new file mode 100644 index 0000000..5869b81 --- /dev/null +++ b/src/client/react/reducers/search.test.ts @@ -0,0 +1,193 @@ +(window).USERS = [ + { type: 's', value: '18561' }, + { type: 's', value: '18562' }, + { type: 's', value: '18563' }, + { type: 's', value: '18564' }, + { type: 's', value: '18565' }, + { type: 's', value: '18566' }, + { type: 's', value: '18567' }, + { type: 's', value: '18568' }, + { type: 's', value: '18569' }, +]; + +const deepFreeze = require('deep-freeze'); +const search = require('./search').default; +const { inputChange, changeSelectedResult } = require('../actions/search'); + +describe('reducers', () => { + describe('search', () => { + describe('SEARCH/INPUT_CHANGE', () => { + it('Returns no results when nothing is typed in', () => { + expect(search(undefined, inputChange(''))).toEqual({ + results: [], + selectedResult: null, + isExactMatch: false, + }); + }); + + it('Returns no results when a space is typed in', () => { + expect(search(undefined, inputChange(' '))).toEqual({ + results: [], + selectedResult: null, + isExactMatch: false, + }); + }); + + it('Preforms a basic search, only returning four results', () => { + expect(search(undefined, inputChange('18'))).toEqual({ + results: [ + 's/18561', + 's/18562', + 's/18563', + 's/18564', + ], + selectedResult: null, + isExactMatch: false, + }); + }); + + it('Selects the first result and sets isExactMatch to true when there is an exact match', () => { + expect(search(undefined, inputChange('18561'))).toEqual({ + results: [ + 's/18561', + ], + selectedResult: 's/18561', + isExactMatch: true, + }); + }); + }); + + describe('SEARCH/CHANGE_SELECTED_RESULT', () => { + it('Does nothing when there are no results', () => { + const prevState = { + results: [], + selectedResult: null, + isExactMatch: false, + }; + + const actionPlus = changeSelectedResult(+1); + const actionMin = changeSelectedResult(-1); + + deepFreeze([prevState, actionPlus, actionMin]); + + const nextStatePlus = search(prevState, actionPlus); + const nextStateMin = search(prevState, actionMin); + expect(nextStatePlus).toEqual(prevState); + expect(nextStateMin).toEqual(prevState); + }); + + it('Does nothing when there is an exact match', () => { + const prevState = { + results: ['s/18561'], + selectedResult: 's/18561', + isExactMatch: true, + }; + + const actionPlus = changeSelectedResult(+1); + const actionMin = changeSelectedResult(-1); + + deepFreeze([prevState, actionPlus, actionMin]); + + const nextStatePlus = search(prevState, actionPlus); + const nextStateMin = search(prevState, actionMin); + + expect(nextStatePlus).toEqual(prevState); + expect(nextStateMin).toEqual(prevState); + }); + + it('Switches to the correct selectedResult when no selected result is selected', () => { + const prevState = { + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: null, + isExactMatch: false, + }; + + const actionPlus = changeSelectedResult(+1); + const actionMin = changeSelectedResult(-1); + + deepFreeze([prevState, actionPlus, actionMin]); + + const nextStatePlus = search(prevState, actionPlus); + const nextStateMin = search(prevState, actionMin); + + expect(nextStatePlus).toEqual({ + ...prevState, + selectedResult: 's/18561', + }); + expect(nextStateMin).toEqual({ + ...prevState, + selectedResult: 's/18563', + }); + }); + + it('Switches to the correct selectedResult when there is a selected result selected', () => { + const prevState = { + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: 's/18562', + isExactMatch: false, + }; + + const actionPlus = changeSelectedResult(+1); + const actionMin = changeSelectedResult(-1); + + deepFreeze([prevState, actionPlus, actionMin]); + + const nextStatePlus = search(prevState, actionPlus); + const nextStateMin = search(prevState, actionMin); + + expect(nextStatePlus).toEqual({ + ...prevState, + selectedResult: 's/18563', + }); + expect(nextStateMin).toEqual({ + ...prevState, + selectedResult: 's/18561', + }); + }); + + it('Properly wraps arround when incrementing', () => { + expect(search({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: 's/18563', + isExactMatch: false, + }, changeSelectedResult(+1))).toEqual({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: null, + isExactMatch: false, + }); + + expect(search({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: null, + isExactMatch: false, + }, changeSelectedResult(+1))).toEqual({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: 's/18561', + isExactMatch: false, + }); + }); + + it('Properly wraps arround when decrementing', () => { + expect(search({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: 's/18561', + isExactMatch: false, + }, changeSelectedResult(-1))).toEqual({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: null, + isExactMatch: false, + }); + + expect(search({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: null, + isExactMatch: false, + }, changeSelectedResult(-1))).toEqual({ + results: ['s/18561', 's/18562', 's/18563'], + selectedResult: 's/18563', + isExactMatch: false, + }); + }); + }); + }); +}); diff --git a/src/client/react/reducers/search.ts b/src/client/react/reducers/search.ts new file mode 100644 index 0000000..658d3ca --- /dev/null +++ b/src/client/react/reducers/search.ts @@ -0,0 +1,90 @@ +import * as fuzzy from 'fuzzy'; +import users, { User } from '../users'; +import { InputChangeAction, ChangeSelectedResultAction } from '../actions/search'; + +export interface State { + results: string[], + selectedResult: string | null, + isExactMatch: boolean, +}; + +export type Action = InputChangeAction | ChangeSelectedResultAction; + +const DEFAULT_STATE: State = { + results: [ + 's/18562', + ], + selectedResult: null, + isExactMatch: false, +}; + +function getSearchResults(allUsers: User[], query: string) { + if (query.trim() === '') { + return []; + } + + const allResults = fuzzy.filter(query, allUsers, { + extract: (user: User) => user.value, + }); + + const firstResults = allResults.splice(0, 4); + const userIds = firstResults.map((result: { original: User }) => result.original.id); + + return userIds; +} + +const search = (state = DEFAULT_STATE, action: Action): State => { + switch (action.type) { + case 'SEARCH/INPUT_CHANGE': { + const results = getSearchResults(users.allUsers, action.typedValue); + let selectedResult = null; + let isExactMatch = false; + + // Is the typed value exactly the same as the first result? Then show the + // appropiate icon instead of the generic search icon. + if ((results.length === 1) && (action.typedValue === users.byId[results[0]].value)) { + [selectedResult] = results; + isExactMatch = true; + } + + return { + ...state, + results, + selectedResult, + isExactMatch, + }; + } + + case 'SEARCH/CHANGE_SELECTED_RESULT': { + const { results, isExactMatch } = state; + + if (isExactMatch) return state; + + const prevSelectedResult = state.selectedResult; + const prevSelectedResultIndex = results.indexOf(prevSelectedResult); + let nextSelectedResultIndex = + prevSelectedResultIndex + action.relativeChange; + + if (nextSelectedResultIndex < -1) { + nextSelectedResultIndex = results.length - 1; + } else if (nextSelectedResultIndex > results.length - 1) { + nextSelectedResultIndex = -1; + } + + const nextSelectedResult = + nextSelectedResultIndex === -1 + ? null + : results[nextSelectedResultIndex]; + + return { + ...state, + selectedResult: nextSelectedResult, + }; + } + + default: + return state; + } +}; + +export default search; -- cgit v1.1