├── .eslintignore ├── .gitignore ├── .prettierrc.js ├── src ├── reducers │ ├── headersReducer.js │ ├── appbaseRefReducer.js │ ├── analyticsRefReducer.js │ ├── appbaseQueryReducer.js │ ├── appliedSettingsReducer.js │ ├── querySuggestionsReducer.js │ ├── defaultPopularSuggestions.js │ ├── componentsReducer.js │ ├── logsReducer.js │ ├── errorReducer.js │ ├── queryReducer.js │ ├── combinedLogsReducer.js │ ├── dependencyTreeReducer.js │ ├── queryOptionsReducer.js │ ├── rawDataReducer.js │ ├── customDataReducer.js │ ├── timestampReducer.js │ ├── customQueryReducer.js │ ├── queryToHitsReducer.js │ ├── recentSearches.js │ ├── defaultQueryReducer.js │ ├── customHighlightReducer.js │ ├── promotedResultsReducer.js │ ├── queryListenerReducer.js │ ├── registeredComponentReducer.js │ ├── propsReducer.js │ ├── mapDataReducer.js │ ├── googleMapScriptReducer.js │ ├── hitsReducer.js │ ├── loadingReducer.js │ ├── aggsReducer.js │ ├── configReducer.js │ ├── compositeAggsReducer.js │ ├── internalValueReducer.js │ ├── watchManReducer.js │ ├── analyticsReducer.js │ ├── aiReducer.js │ ├── valueReducer.js │ └── index.js ├── actions │ ├── index.js │ ├── component.js │ ├── props.js │ ├── maps.js │ ├── hits.js │ ├── value.js │ ├── misc.js │ ├── analytics.js │ └── utils.js ├── utils │ ├── dateFormats.js │ ├── causes.js │ ├── __tests__ │ │ ├── getInnerKey.js │ │ ├── __snapshots__ │ │ │ └── analytics.js.snap │ │ ├── isEqual.js │ │ └── analytics.js │ ├── graphQL.js │ ├── middlewares.js │ ├── polyfills.js │ ├── analytics.js │ ├── constants.js │ ├── types.js │ ├── suggestions.js │ ├── server.js │ ├── diacritics.js │ └── transform.js ├── index.js └── constants │ └── index.js ├── .editorconfig ├── CHANGELOG.md ├── .eslintrc ├── rollup.config.mjs ├── package.json ├── README.md └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | cjs/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.* 3 | *.swp 4 | .DS_Store 5 | **/.DS_Store 6 | *.log 7 | logs 8 | lib 9 | coverage 10 | cjs 11 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 4, 3 | printWidth: 100, 4 | singleQuote: true, 5 | trailingComma: 'all', 6 | useTabs: true, 7 | }; 8 | -------------------------------------------------------------------------------- /src/reducers/headersReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_HEADERS } from '../constants'; 2 | 3 | export default function headersReducer(state = {}, action) { 4 | if (action.type === SET_HEADERS) { 5 | return action.headers; 6 | } 7 | return state; 8 | } 9 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './component'; 2 | export * from './hits'; 3 | export * from './maps'; 4 | export * from './query'; 5 | export * from './value'; 6 | export * from './props'; 7 | export * from './analytics'; 8 | export * from './misc'; 9 | -------------------------------------------------------------------------------- /src/reducers/appbaseRefReducer.js: -------------------------------------------------------------------------------- 1 | import { ADD_APPBASE_REF } from '../constants'; 2 | 3 | export default function appbaseRefReducer(state = {}, action) { 4 | if (action.type === ADD_APPBASE_REF) { 5 | return action.appbaseRef; 6 | } 7 | return state; 8 | } 9 | -------------------------------------------------------------------------------- /src/reducers/analyticsRefReducer.js: -------------------------------------------------------------------------------- 1 | import { ADD_ANALYTICS_REF } from '../constants'; 2 | 3 | export default function analyticsRefReducer(state = {}, action) { 4 | if (action.type === ADD_ANALYTICS_REF) { 5 | return action.analyticsRef; 6 | } 7 | return state; 8 | } 9 | -------------------------------------------------------------------------------- /src/reducers/appbaseQueryReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_APPBASE_QUERY } from '../constants'; 2 | 3 | export default function appbaseQueryReducer(state = {}, action) { 4 | if (action.type === SET_APPBASE_QUERY) { 5 | return { ...state, ...action.query }; 6 | } 7 | return state; 8 | } 9 | -------------------------------------------------------------------------------- /src/reducers/appliedSettingsReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_APPLIED_SETTINGS } from '../constants'; 2 | 3 | export default function appliedSettingsReducer(state = {}, action) { 4 | if (action.type === SET_APPLIED_SETTINGS) { 5 | return { 6 | ...state, 7 | [action.component]: action.data, 8 | }; 9 | } 10 | 11 | return state; 12 | } 13 | -------------------------------------------------------------------------------- /src/reducers/querySuggestionsReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_POPULAR_SUGGESTIONS } from '../constants'; 2 | 3 | export default function querySuggestionsReducer(state = {}, action) { 4 | if (action.type === SET_POPULAR_SUGGESTIONS) { 5 | return { 6 | ...state, 7 | [action.component]: action.suggestions, 8 | }; 9 | } 10 | 11 | return state; 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | 9 | # Matches multiple files with brace expansion notation 10 | # Set default charset 11 | [*.{js,jsx,html,sass}] 12 | charset = utf-8 13 | indent_style = tab 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /src/reducers/defaultPopularSuggestions.js: -------------------------------------------------------------------------------- 1 | import { SET_DEFAULT_POPULAR_SUGGESTIONS } from '../constants'; 2 | 3 | export default function defaultPopularSuggestions(state = {}, action) { 4 | if (action.type === SET_DEFAULT_POPULAR_SUGGESTIONS) { 5 | return { 6 | ...state, 7 | [action.component]: action.suggestions, 8 | }; 9 | } 10 | 11 | return state; 12 | } 13 | -------------------------------------------------------------------------------- /src/reducers/componentsReducer.js: -------------------------------------------------------------------------------- 1 | import { ADD_COMPONENT, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function componentsReducer(state = [], action) { 4 | if (action.type === ADD_COMPONENT) { 5 | return [...state, action.component]; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | return state.filter(element => element !== action.component); 8 | } 9 | return state; 10 | } 11 | -------------------------------------------------------------------------------- /src/reducers/logsReducer.js: -------------------------------------------------------------------------------- 1 | import { LOG_QUERY, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function logsReducer(state = {}, action) { 4 | if (action.type === LOG_QUERY) { 5 | return { ...state, [action.component]: action.query }; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | const { [action.component]: del, ...obj } = state; 8 | return obj; 9 | } 10 | return state; 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/errorReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_ERROR, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function errorReducer(state = {}, action) { 4 | if (action.type === SET_ERROR) { 5 | return { ...state, [action.component]: action.error }; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | const { [action.component]: del, ...obj } = state; 8 | return obj; 9 | } 10 | return state; 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/queryReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_QUERY, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function queryReducer(state = {}, action) { 4 | if (action.type === SET_QUERY) { 5 | return { ...state, [action.component]: action.query }; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | const { [action.component]: del, ...obj } = state; 8 | return obj; 9 | } 10 | return state; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/dateFormats.js: -------------------------------------------------------------------------------- 1 | const dateFormats = { 2 | date: 'YYYY-MM-DD', 3 | basic_date: 'YYYYMMDD', 4 | basic_date_time: 'YYYYMMDD[T]HHmmss.SSSZ', 5 | basic_date_time_no_millis: 'YYYYMMDD[T]HHmmssZ', 6 | date_time_no_millis: 'YYYY-MM-DD[T]HH:mm:ssZ', 7 | basic_time: 'HHmmss.SSSZ', 8 | basic_time_no_millis: 'HHmmssZ', 9 | epoch_millis: 'epoch_millis', 10 | epoch_second: 'epoch_second', 11 | }; 12 | 13 | export default dateFormats; 14 | -------------------------------------------------------------------------------- /src/reducers/combinedLogsReducer.js: -------------------------------------------------------------------------------- 1 | import { LOG_COMBINED_QUERY, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function combinedLogsReducer(state = {}, action) { 4 | if (action.type === LOG_COMBINED_QUERY) { 5 | return { ...state, [action.component]: action.query }; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | const { [action.component]: del, ...obj } = state; 8 | return obj; 9 | } 10 | return state; 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/dependencyTreeReducer.js: -------------------------------------------------------------------------------- 1 | import { WATCH_COMPONENT, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function dependencyTreeReducer(state = {}, action) { 4 | if (action.type === WATCH_COMPONENT) { 5 | return { ...state, [action.component]: action.react }; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | const { [action.component]: del, ...obj } = state; 8 | return obj; 9 | } 10 | return state; 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/queryOptionsReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_QUERY_OPTIONS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function queryOptionsReducer(state = {}, action) { 4 | if (action.type === SET_QUERY_OPTIONS) { 5 | return { ...state, [action.component]: action.options }; 6 | } else if (action.type === REMOVE_COMPONENT) { 7 | const { [action.component]: del, ...obj } = state; 8 | return obj; 9 | } 10 | return state; 11 | } 12 | -------------------------------------------------------------------------------- /src/reducers/rawDataReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_RAW_DATA, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function rawDataReducer(state = {}, action) { 4 | if (action.type === SET_RAW_DATA) { 5 | return { 6 | ...state, 7 | [action.component]: action.response, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | return state; 14 | } 15 | -------------------------------------------------------------------------------- /src/reducers/customDataReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_CUSTOM_DATA, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function customDataReducer(state = {}, action) { 4 | if (action.type === SET_CUSTOM_DATA) { 5 | return { 6 | ...state, 7 | [action.component]: action.data, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | return state; 14 | } 15 | -------------------------------------------------------------------------------- /src/reducers/timestampReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_TIMESTAMP, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function timestampReducer(state = {}, action) { 4 | if (action.type === SET_TIMESTAMP) { 5 | return { 6 | ...state, 7 | [action.component]: action.timestamp, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | return state; 14 | } 15 | -------------------------------------------------------------------------------- /src/reducers/customQueryReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_CUSTOM_QUERY, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function customQueryReducer(state = {}, action) { 4 | if (action.type === SET_CUSTOM_QUERY) { 5 | return { 6 | ...state, 7 | [action.component]: action.query, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/queryToHitsReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_QUERY_TO_HITS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function queryToHitsReducer(state = {}, action) { 4 | if (action.type === SET_QUERY_TO_HITS) { 5 | return { 6 | ...state, 7 | [action.component]: action.query, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/recentSearches.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECENT_SEARCHES_SUCCESS, 3 | RECENT_SEARCHES_ERROR, 4 | } from '../constants'; 5 | 6 | export default function recentSearchesReducer(state = {}, action) { 7 | if (action.type === RECENT_SEARCHES_SUCCESS) { 8 | return { 9 | error: null, 10 | data: action.data, 11 | }; 12 | } else if (action.type === RECENT_SEARCHES_ERROR) { 13 | return { 14 | error: action.error, 15 | }; 16 | } 17 | return state; 18 | } 19 | -------------------------------------------------------------------------------- /src/reducers/defaultQueryReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_DEFAULT_QUERY, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function defaultQueryReducer(state = {}, action) { 4 | if (action.type === SET_DEFAULT_QUERY) { 5 | return { 6 | ...state, 7 | [action.component]: action.query, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/causes.js: -------------------------------------------------------------------------------- 1 | // A map of causes leading to changes in components 2 | 3 | const ENTER_PRESS = 'ENTER_PRESS'; 4 | const SUGGESTION_SELECT = 'SUGGESTION_SELECT'; 5 | const CLEAR_VALUE = 'CLEAR_VALUE'; 6 | const SEARCH_ICON_CLICK = 'SEARCH_ICON_CLICK'; 7 | const CLEAR_ALL_TAGS = 'CLEAR_ALL_TAGS'; 8 | 9 | const causes = { 10 | ENTER_PRESS, 11 | SUGGESTION_SELECT, 12 | CLEAR_VALUE, 13 | SEARCH_ICON_CLICK, 14 | CLEAR_ALL_TAGS, 15 | }; 16 | 17 | export default causes; 18 | -------------------------------------------------------------------------------- /src/reducers/customHighlightReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_CUSTOM_HIGHLIGHT_OPTIONS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function customHighlightReducer(state = {}, action) { 4 | if (action.type === SET_CUSTOM_HIGHLIGHT_OPTIONS) { 5 | return { 6 | ...state, 7 | [action.component]: action.data, 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/__tests__/getInnerKey.js: -------------------------------------------------------------------------------- 1 | import { getInnerKey } from '../helper'; 2 | 3 | test('returns correct key when it is present in the given object', () => { 4 | const value = 'hello'; 5 | const obj = { 6 | value, 7 | }; 8 | 9 | expect(getInnerKey(obj, 'value')).toBe('hello'); 10 | }); 11 | 12 | test('returns empty object when key is not present in the given object', () => { 13 | const obj = { 14 | value: 'hello', 15 | }; 16 | 17 | expect(getInnerKey(obj, 'hello')).toEqual({}); 18 | }); 19 | -------------------------------------------------------------------------------- /src/reducers/promotedResultsReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_PROMOTED_RESULTS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function promotedResultsReducer(state = {}, action) { 4 | if (action.type === SET_PROMOTED_RESULTS) { 5 | return { 6 | ...state, 7 | [action.component]: action.results.map(item => ({ ...item, _promoted: true })), 8 | }; 9 | } else if (action.type === REMOVE_COMPONENT) { 10 | const { [action.component]: del, ...obj } = state; 11 | return obj; 12 | } 13 | 14 | return state; 15 | } 16 | -------------------------------------------------------------------------------- /src/reducers/queryListenerReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_QUERY_LISTENER, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function queryListenerReducer(state = {}, action) { 4 | if (action.type === SET_QUERY_LISTENER) { 5 | return { 6 | ...state, 7 | [action.component]: { 8 | onQueryChange: action.onQueryChange, 9 | onError: action.onError, 10 | }, 11 | }; 12 | } else if (action.type === REMOVE_COMPONENT) { 13 | const { [action.component]: del, ...obj } = state; 14 | return obj; 15 | } 16 | return state; 17 | } 18 | -------------------------------------------------------------------------------- /src/reducers/registeredComponentReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_REGISTERED_COMPONENT_TIMESTAMP, 3 | REMOVE_REGISTERED_COMPONENT_TIMESTAMP, 4 | } from '../constants'; 5 | 6 | export default function timestampReducer(state = {}, action) { 7 | if (action.type === SET_REGISTERED_COMPONENT_TIMESTAMP) { 8 | return { 9 | ...state, 10 | [action.component]: action.timestamp, 11 | }; 12 | } else if (action.type === REMOVE_REGISTERED_COMPONENT_TIMESTAMP) { 13 | const { [action.component]: del, ...obj } = state; 14 | return obj; 15 | } 16 | return state; 17 | } 18 | -------------------------------------------------------------------------------- /src/reducers/propsReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_PROPS, UPDATE_PROPS, REMOVE_PROPS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function queryOptionsReducer(state = {}, action) { 4 | switch (action.type) { 5 | case SET_PROPS: 6 | return { ...state, [action.component]: action.options }; 7 | case UPDATE_PROPS: 8 | return { 9 | ...state, 10 | [action.component]: { ...state[action.component], ...action.options }, 11 | }; 12 | case REMOVE_PROPS: 13 | case REMOVE_COMPONENT: { 14 | const { [action.component]: del, ...obj } = state; 15 | return obj; 16 | } 17 | default: 18 | return state; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/reducers/mapDataReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_MAP_DATA, REMOVE_COMPONENT, SET_MAP_RESULTS } from '../constants'; 2 | 3 | export default function mapDataReducer(state = {}, action) { 4 | if (action.type === SET_MAP_DATA) { 5 | return { 6 | ...state, 7 | [action.componentId]: { 8 | query: action.query, 9 | persistMapQuery: action.persistMapQuery, 10 | }, 11 | }; 12 | } else if (action.type === SET_MAP_RESULTS) { 13 | return { 14 | ...state, 15 | [action.componentId]: { 16 | ...state[action.componentId], 17 | ...action.payload, 18 | }, 19 | }; 20 | } else if (action.type === REMOVE_COMPONENT) { 21 | const { [action.component]: del, ...obj } = state; 22 | return obj; 23 | } 24 | return state; 25 | } 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 |
6 | The following changes have been included in the master branch and will be out in the next release. Click to expand 7 | - Fix an edge case with generating suggestions with numeric splits [#8](https://github.com/appbaseio/reactivecore/issues/8) 8 |
9 | 10 | ## v1.5.2 11 | - Fix an edge case with nested multifield suggestions [here](https://github.com/appbaseio/reactivecore/commit/be9e2cc79fcf0d3b11f6e55eb1b1af29e49a4003) 12 | 13 | ## v1.5.1 14 | - Fix suggestions for nested fields [#5](https://github.com/appbaseio/reactivecore/pull/5) 15 | 16 | ## v1.5.0 17 | - Added support for msearch (enabling combined queries) 18 | - Fixed support for query-listeners (onQueryChange) 19 | -------------------------------------------------------------------------------- /src/reducers/googleMapScriptReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_GOOGLE_MAP_SCRIPT_ERROR, 3 | SET_GOOGLE_MAP_SCRIPT_LOADED, 4 | SET_GOOGLE_MAP_SCRIPT_LOADING, 5 | } from '../constants'; 6 | 7 | const INITIAL_STATE = { 8 | loading: false, 9 | loaded: false, 10 | error: null, 11 | }; 12 | 13 | export default function googleMapScriptReducer(state = INITIAL_STATE, action) { 14 | const { 15 | type, loading, loaded, error, 16 | } = action; 17 | if (type === SET_GOOGLE_MAP_SCRIPT_LOADING) { 18 | return { 19 | ...INITIAL_STATE, 20 | loading, 21 | }; 22 | } else if (type === SET_GOOGLE_MAP_SCRIPT_LOADED) { 23 | return { 24 | ...INITIAL_STATE, 25 | loaded, 26 | }; 27 | } else if (type === SET_GOOGLE_MAP_SCRIPT_ERROR) { 28 | return { 29 | ...INITIAL_STATE, 30 | error, 31 | }; 32 | } 33 | 34 | return state; 35 | } 36 | -------------------------------------------------------------------------------- /src/reducers/hitsReducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_HITS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function hitsReducer(state = {}, action) { 4 | if (action.type === UPDATE_HITS) { 5 | if (action.append) { 6 | return { 7 | ...state, 8 | [action.component]: { 9 | hits: [...state[action.component].hits, ...action.hits], 10 | total: action.total, 11 | time: action.time, 12 | hidden: action.hidden || 0, 13 | }, 14 | }; 15 | } 16 | return { 17 | ...state, 18 | [action.component]: { 19 | hits: action.hits, 20 | total: action.total, 21 | time: action.time, 22 | hidden: action.hidden || 0, 23 | }, 24 | }; 25 | } else if (action.type === REMOVE_COMPONENT) { 26 | const { [action.component]: del, ...obj } = state; 27 | return obj; 28 | } 29 | return state; 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/graphQL.js: -------------------------------------------------------------------------------- 1 | import fetch from 'cross-fetch'; 2 | 3 | const fetchGraphQL = (requestOptions) => { 4 | const { 5 | graphQLUrl, url, credentials, app, query, headers, 6 | } = requestOptions; 7 | const fetchUrl = credentials ? url.replace('//', `//${credentials}@`) : url; 8 | return fetch(graphQLUrl, { 9 | method: 'POST', 10 | body: ` 11 | query{ 12 | elastic77(host: "${fetchUrl}"){ 13 | msearch( 14 | index: "${app}" 15 | body: ${JSON.stringify(query.map(item => JSON.stringify(item)))} 16 | ) 17 | } 18 | } 19 | `, 20 | headers: { 21 | ...headers, 22 | 'Content-Type': 'application/graphql', 23 | }, 24 | }) 25 | .then(res => res.json()) 26 | .then(jsonRes => jsonRes.data.elastic77.msearch) 27 | .catch((error) => { 28 | console.error(error); 29 | }); 30 | }; 31 | 32 | export default fetchGraphQL; 33 | -------------------------------------------------------------------------------- /src/reducers/loadingReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_LOADING, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function loadingReducer(state = {}, action) { 4 | if (action.type === SET_LOADING) { 5 | let requestCount = state[`${action.component}_active`] || 0; 6 | if (action.isLoading) { 7 | requestCount += 1; 8 | } else if (requestCount) { 9 | requestCount -= 1; 10 | } 11 | return { 12 | ...state, 13 | [action.component]: action.isLoading, 14 | [`${action.component}_active`]: requestCount, 15 | // record the timestamp for the latest request 16 | ...action.isLoading ? { [`${action.component}_timestamp`]: new Date().getTime() } : null, 17 | }; 18 | } else if (action.type === REMOVE_COMPONENT) { 19 | const { [action.component]: del, [`${action.component}_active`]: del2, ...obj } = state; 20 | return obj; 21 | } 22 | return state; 23 | } 24 | -------------------------------------------------------------------------------- /src/reducers/aggsReducer.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_AGGS, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function aggsReducer(state = {}, action) { 4 | if (action.type === UPDATE_AGGS) { 5 | if (action.append) { 6 | const field = Object.keys(state[action.component])[0]; 7 | const { buckets: newBuckets, ...aggsData } = action.aggregations[field]; 8 | // console.log('received aggs', action.aggregations); 9 | return { 10 | ...state, 11 | [action.component]: { 12 | [field]: { 13 | buckets: [...state[action.component][field].buckets, ...newBuckets], 14 | ...aggsData, 15 | }, 16 | }, 17 | }; 18 | } 19 | return { ...state, [action.component]: action.aggregations }; 20 | } else if (action.type === REMOVE_COMPONENT) { 21 | const { [action.component]: del, ...obj } = state; 22 | return obj; 23 | } 24 | return state; 25 | } 26 | -------------------------------------------------------------------------------- /src/reducers/configReducer.js: -------------------------------------------------------------------------------- 1 | import { ADD_CONFIG, UPDATE_ANALYTICS_CONFIG, UPDATE_CONFIG } from '../constants'; 2 | import { defaultAnalyticsConfig } from '../utils/analytics'; 3 | 4 | export default function configReducer( 5 | state = { 6 | analyticsConfig: defaultAnalyticsConfig, 7 | lock: false, 8 | }, 9 | action, 10 | ) { 11 | if (action.type === ADD_CONFIG) { 12 | return { 13 | ...state, 14 | analyticsConfig: { 15 | ...defaultAnalyticsConfig, 16 | ...action.analyticsConfig, 17 | }, 18 | }; 19 | } else if (action.type === UPDATE_ANALYTICS_CONFIG) { 20 | return { 21 | ...state, 22 | analyticsConfig: { 23 | ...state.analyticsConfig, 24 | ...action.analyticsConfig, 25 | }, 26 | }; 27 | } else if (action.type === UPDATE_CONFIG) { 28 | return { 29 | ...state, 30 | ...action.config, 31 | }; 32 | } 33 | return state; 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/__tests__/__snapshots__/analytics.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot filter components 1`] = ` 4 | Array [ 5 | "NUMBERBOX", 6 | "TAGCLOUD", 7 | "TOGGLEBUTTON", 8 | "DATEPICKER", 9 | "DATERANGE", 10 | "MULTIDATALIST", 11 | "MULTIDROPDOWNLIST", 12 | "MULTILIST", 13 | "SINGLEDATALIST", 14 | "SINGLEDROPDOWNLIST", 15 | "SINGLELIST", 16 | "DYNAMICRANGESLIDER", 17 | "MULTIDROPDOWNRANGE", 18 | "MULTIRANGE", 19 | "RANGESLIDER", 20 | "RATINGSFILTER", 21 | "SINGLEDROPDOWNRANGE", 22 | "SINGLERANGE", 23 | ] 24 | `; 25 | 26 | exports[`snapshot range components 1`] = ` 27 | Array [ 28 | "DATERANGE", 29 | "DYNAMICRANGESLIDER", 30 | "RANGESLIDER", 31 | "RANGEINPUT", 32 | "RATINGSFILTER", 33 | ] 34 | `; 35 | 36 | exports[`snapshot range object components 1`] = ` 37 | Array [ 38 | "SINGLERANGE", 39 | "SINGLEDROPDOWNRANGE", 40 | "MULTIRANGE", 41 | "MULTIDROPDOWNRANGE", 42 | ] 43 | `; 44 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "plugins": ["jest"], 8 | "extends": ["airbnb", "plugin:jest/recommended"], 9 | "parser": "babel-eslint", 10 | "rules": { 11 | "indent": [2, "tab", { "SwitchCase": 1, "VariableDeclarator": 1 }], 12 | "no-tabs": 0, 13 | "no-mixed-spaces-and-tabs": 0, // disable rule 14 | "no-underscore-dangle": 0, 15 | "operator-linebreak": ["error", "before"], 16 | "prefer-destructuring": 0, 17 | "no-console": ["error", { "allow": ["warn", "error"] }], 18 | "class-methods-use-this": 0, 19 | "no-prototype-builtins": 0, 20 | "no-plusplus": 0, 21 | 22 | "react/jsx-indent": [2, "tab"], 23 | "react/jsx-indent-props": [2, "tab"], 24 | "react/no-danger": 0, 25 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 26 | "react/no-typos": 0, 27 | "react/require-default-props": 0, 28 | "react/no-unused-prop-types": 0, 29 | "react/sort-comp": 0 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/__tests__/isEqual.js: -------------------------------------------------------------------------------- 1 | import { isEqual } from '../helper'; 2 | 3 | test('returns true for matching objects', () => { 4 | const object1 = { a: 'xyz', b: 123 }; 5 | const object2 = { b: 123, a: 'xyz' }; 6 | const result = isEqual(object1, object2); 7 | expect(result).toBe(true); 8 | }); 9 | 10 | test('returns true for same objects', () => { 11 | const object1 = { a: 'xyz', b: 123 }; 12 | const object2 = object1; 13 | const result = isEqual(object1, object2); 14 | expect(result).toBe(true); 15 | }); 16 | 17 | test('returns true for matching arrays', () => { 18 | const array1 = ['abc', 123]; 19 | const array2 = ['abc', 123]; 20 | const result = isEqual(array1, array2); 21 | expect(result).toBe(true); 22 | }); 23 | 24 | test('returns true for same arrays', () => { 25 | const array1 = ['abc', 123]; 26 | const array2 = array1; 27 | const result = isEqual(array1, array2); 28 | expect(result).toBe(true); 29 | }); 30 | 31 | test('returns true for primitive types', () => { 32 | const val1 = 'abc'; 33 | const val2 = 'abc'; 34 | const result = isEqual(val1, val2); 35 | expect(result).toBe(true); 36 | }); 37 | -------------------------------------------------------------------------------- /src/actions/component.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_COMPONENT, 3 | REMOVE_COMPONENT, 4 | WATCH_COMPONENT, 5 | SET_REGISTERED_COMPONENT_TIMESTAMP, 6 | } from '../constants'; 7 | 8 | import { executeQuery } from './query'; 9 | 10 | function addComponentToList(component) { 11 | return { 12 | type: ADD_COMPONENT, 13 | component, 14 | }; 15 | } 16 | 17 | function addComponentTimestamp(component, timestamp) { 18 | return { 19 | type: SET_REGISTERED_COMPONENT_TIMESTAMP, 20 | component, 21 | timestamp, 22 | }; 23 | } 24 | export function addComponent(component, timestamp) { 25 | return (dispatch) => { 26 | dispatch(addComponentToList(component)); 27 | dispatch(addComponentTimestamp(component, timestamp)); 28 | }; 29 | } 30 | 31 | export function removeComponent(component) { 32 | return { 33 | type: REMOVE_COMPONENT, 34 | component, 35 | }; 36 | } 37 | 38 | function updateWatchman(component, react) { 39 | return { 40 | type: WATCH_COMPONENT, 41 | component, 42 | react, 43 | }; 44 | } 45 | 46 | export function watchComponent(component, react, execute = true) { 47 | return (dispatch) => { 48 | dispatch(updateWatchman(component, react)); 49 | if (execute) dispatch(executeQuery(component)); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/reducers/compositeAggsReducer.js: -------------------------------------------------------------------------------- 1 | // get top hits from composite aggregations response 2 | import { UPDATE_COMPOSITE_AGGS } from '../constants'; 3 | 4 | export default function compositeAggsReducer(state = {}, action) { 5 | if (action.type === UPDATE_COMPOSITE_AGGS) { 6 | const aggsResponse 7 | = Object.values(action.aggregations) && Object.values(action.aggregations)[0]; 8 | const fieldName = Object.keys(action.aggregations)[0]; 9 | if (!aggsResponse) return state; 10 | let buckets = []; 11 | if (aggsResponse.buckets && Array.isArray(aggsResponse.buckets)) { 12 | buckets = aggsResponse.buckets; 13 | } 14 | const parsedAggs = buckets.map((bucket) => { 15 | // eslint-disable-next-line camelcase 16 | const { doc_count, key, [fieldName]: hitsData } = bucket; 17 | let flatData = {}; 18 | let _source = {}; 19 | if (hitsData && hitsData.hits) { 20 | ({ _source, ...flatData } = hitsData.hits.hits[0]); 21 | } 22 | return { 23 | _doc_count: doc_count, 24 | _key: typeof key === 'string' ? key : key[fieldName], 25 | top_hits: hitsData, 26 | ...flatData, 27 | ..._source, 28 | }; 29 | }); 30 | return { 31 | ...state, 32 | [action.component]: action.append 33 | ? [...state[action.component], ...parsedAggs] 34 | : parsedAggs, 35 | }; 36 | } 37 | return state; 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import babel from '@rollup/plugin-babel'; 5 | import { glob } from 'glob'; 6 | import json from '@rollup/plugin-json'; 7 | 8 | const files = glob.sync('src/**/*.js'); // Get all .js files in src directory and its subdirectories 9 | 10 | export default files.map(file => ({ 11 | input: file, 12 | output: [ 13 | { 14 | file: `lib/${file.substring(4).replace(/\.js$/, '.mjs')}`, // Remove "src/" from the path and replace .js with .mjs 15 | format: 'es', 16 | sourcemap: false, 17 | }, 18 | { 19 | file: `lib/${file.substring(4)}`, // Remove "src/" from the path 20 | format: 'cjs', 21 | sourcemap: false, 22 | }, 23 | ], 24 | plugins: [ 25 | resolve(), 26 | commonjs(), 27 | babel({ 28 | babelHelpers: 'runtime', 29 | exclude: 'node_modules/**', 30 | presets: [ 31 | [ 32 | '@babel/preset-env', 33 | { 34 | modules: false, 35 | }, 36 | ], 37 | ], 38 | plugins: [ 39 | [ 40 | '@babel/plugin-transform-runtime', 41 | { 42 | corejs: false, 43 | helpers: true, 44 | regenerator: true, 45 | useESModules: true, 46 | }, 47 | ], 48 | ], 49 | }), 50 | json(), 51 | ], 52 | })); 53 | -------------------------------------------------------------------------------- /src/utils/middlewares.js: -------------------------------------------------------------------------------- 1 | import { SET_AI_RESPONSE, SET_AI_RESPONSE_DELAYED, SET_AI_RESPONSE_LOADING } from '../constants'; 2 | 3 | export const aiResponseMiddleware = store => (next) => { 4 | const pendingActions = {}; 5 | const isDispatching = {}; 6 | 7 | const dispatchNextAction = (componentId) => { 8 | if (pendingActions[componentId].length === 0 || isDispatching[componentId]) { 9 | return; 10 | } 11 | 12 | isDispatching[componentId] = true; 13 | const action = pendingActions[componentId].shift(); 14 | requestAnimationFrame(() => { 15 | store.dispatch(action); 16 | isDispatching[componentId] = false; 17 | dispatchNextAction(componentId); 18 | }); 19 | }; 20 | 21 | return (action) => { 22 | if (action.type === SET_AI_RESPONSE_DELAYED) { 23 | const { component } = action; 24 | if (!pendingActions[component]) { 25 | pendingActions[component] = []; 26 | isDispatching[component] = false; 27 | } 28 | pendingActions[component].push({ ...action, type: SET_AI_RESPONSE }); 29 | dispatchNextAction(component); 30 | } else if (action.type === SET_AI_RESPONSE_LOADING && action.isLoading) { 31 | const { component } = action; 32 | if (pendingActions[component]) { 33 | pendingActions[component] = []; 34 | isDispatching[component] = false; 35 | } 36 | } 37 | return next(action); 38 | }; 39 | }; 40 | 41 | export default aiResponseMiddleware; 42 | -------------------------------------------------------------------------------- /src/actions/props.js: -------------------------------------------------------------------------------- 1 | import { SET_PROPS, REMOVE_PROPS, UPDATE_PROPS } from '../constants'; 2 | import { validProps } from '../utils/constants'; 3 | 4 | const getfilteredOptions = (options = {}) => { 5 | const filteredOptions = {}; 6 | Object.keys(options).forEach((option) => { 7 | if (validProps.includes(option)) { 8 | filteredOptions[option] = options[option]; 9 | } 10 | }); 11 | return filteredOptions; 12 | }; 13 | /** 14 | * Sets the external props for a component 15 | * @param {String} component 16 | * @param {Object} options 17 | */ 18 | export function setComponentProps(component, options, componentType) { 19 | return { 20 | type: SET_PROPS, 21 | component, 22 | options: getfilteredOptions({ ...options, componentType }), 23 | }; 24 | } 25 | 26 | /** 27 | * Updates the external props for a component, overrides the duplicates 28 | * @param {String} component 29 | * @param {Object} options 30 | */ 31 | export function updateComponentProps(component, options, componentType) { 32 | return { 33 | type: UPDATE_PROPS, 34 | component, 35 | options: getfilteredOptions({ ...options, componentType }), 36 | }; 37 | } 38 | 39 | /** 40 | * Remove the external props for a component 41 | * @param {String} component 42 | * @param {Object} options 43 | */ 44 | export function removeComponentProps(component) { 45 | return { 46 | type: REMOVE_PROPS, 47 | component, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/reducers/internalValueReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_INTERNAL_VALUE, RESET_TO_DEFAULT, CLEAR_VALUES, REMOVE_COMPONENT } from '../constants'; 2 | 3 | export default function valueReducer(state = {}, action) { 4 | switch (action.type) { 5 | case SET_INTERNAL_VALUE: 6 | return { 7 | ...state, 8 | [action.component]: { 9 | value: action.value, 10 | componentType: action.componentType, 11 | category: action.category, 12 | meta: action.meta, 13 | }, 14 | }; 15 | case CLEAR_VALUES: { 16 | const nextState = {}; 17 | if (action.resetValues) { 18 | Object.keys(action.resetValues).forEach((componentId) => { 19 | nextState[componentId] = { 20 | ...state[componentId], 21 | value: action.resetValues[componentId], 22 | }; 23 | }); 24 | } 25 | // clearAllBlacklistComponents has more priority over reset values 26 | if (Array.isArray(action.clearAllBlacklistComponents)) { 27 | Object.keys(state).forEach((componentId) => { 28 | if (action.clearAllBlacklistComponents.includes(componentId)) { 29 | nextState[componentId] = state[componentId]; 30 | } 31 | }); 32 | } 33 | return nextState; 34 | } 35 | case RESET_TO_DEFAULT: 36 | return { 37 | ...state, 38 | ...action.defaultValues, 39 | }; 40 | case REMOVE_COMPONENT: { 41 | const { [action.component]: del, ...obj } = state; 42 | return obj; 43 | } 44 | default: 45 | return state; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/reducers/watchManReducer.js: -------------------------------------------------------------------------------- 1 | import { WATCH_COMPONENT } from '../constants'; 2 | 3 | function getWatchList(depTree) { 4 | const list = Object.values(depTree); 5 | const components = []; 6 | 7 | list.forEach((item) => { 8 | if (typeof item === 'string') { 9 | components.push(item); 10 | } else if (Array.isArray(item)) { 11 | item.forEach((component) => { 12 | if (typeof component === 'string') { 13 | components.push(component); 14 | } else { 15 | // in this case, we have { : <> } object inside the array 16 | components.push(...getWatchList(component)); 17 | } 18 | }); 19 | } else if (typeof item === 'object' && item !== null) { 20 | components.push(...getWatchList(item)); 21 | } 22 | }); 23 | 24 | return components.filter((value, index, array) => array.indexOf(value) === index); 25 | } 26 | 27 | export default function watchManReducer(state = {}, action) { 28 | if (action.type === WATCH_COMPONENT) { 29 | const watchList = getWatchList(action.react); 30 | const newState = { ...state }; 31 | 32 | Object.keys(newState).forEach((key) => { 33 | newState[key] = newState[key].filter(value => value !== action.component); 34 | }); 35 | 36 | watchList.forEach((item) => { 37 | if (Array.isArray(newState[item])) { 38 | newState[item] = [...newState[item], action.component]; 39 | } else { 40 | newState[item] = [action.component]; 41 | } 42 | }); 43 | return newState; 44 | } 45 | return state; 46 | } 47 | -------------------------------------------------------------------------------- /src/reducers/analyticsReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_VALUE, 3 | SET_SEARCH_ID, 4 | SET_SUGGESTIONS_SEARCH_VALUE, 5 | CLEAR_SUGGESTIONS_SEARCH_VALUE, 6 | SET_SUGGESTIONS_SEARCH_ID, 7 | } from '../constants'; 8 | import { componentTypes } from '../utils/constants'; 9 | 10 | const initialState = { 11 | searchValue: null, 12 | searchId: null, 13 | // Maintain the suggestions analytics separately 14 | suggestionsSearchId: null, 15 | suggestionsSearchValue: null, 16 | }; 17 | 18 | const searchComponents = [componentTypes.dataSearch, componentTypes.categorySearch]; 19 | 20 | export default function analyticsReducer(state = initialState, action) { 21 | switch (action.type) { 22 | case SET_VALUE: 23 | if (searchComponents.includes(action.componentType)) { 24 | return { 25 | searchValue: action.value, 26 | searchId: null, 27 | }; 28 | } 29 | return state; 30 | case SET_SEARCH_ID: 31 | return { 32 | ...state, 33 | searchId: action.searchId, 34 | }; 35 | case SET_SUGGESTIONS_SEARCH_VALUE: 36 | return { 37 | ...state, 38 | suggestionsSearchValue: action.value, 39 | suggestionsSearchId: null, 40 | }; 41 | case SET_SUGGESTIONS_SEARCH_ID: 42 | return { 43 | ...state, 44 | suggestionsSearchId: action.searchId, 45 | }; 46 | case CLEAR_SUGGESTIONS_SEARCH_VALUE: 47 | return { 48 | ...state, 49 | suggestionsSearchValue: null, 50 | suggestionsSearchId: null, 51 | }; 52 | default: 53 | return state; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/actions/maps.js: -------------------------------------------------------------------------------- 1 | import { SET_MAP_DATA, SET_MAP_RESULTS } from '../constants'; 2 | import { executeQuery } from './query'; 3 | import { setInternalValue } from './value'; 4 | import { getInternalComponentID } from '../utils/transform'; 5 | import { componentTypes } from '../utils/constants'; 6 | 7 | export function updateMapData(componentId, query, persistMapQuery) { 8 | return { 9 | type: SET_MAP_DATA, 10 | componentId, 11 | query, 12 | persistMapQuery, 13 | }; 14 | } 15 | 16 | export function setMapData( 17 | componentId, 18 | query, 19 | persistMapQuery, 20 | forceExecute, 21 | meta = {}, 22 | // A unique identifier for map query to recognize the results for latest requests 23 | queryId = '', 24 | ) { 25 | return (dispatch) => { 26 | dispatch(updateMapData(componentId, query, persistMapQuery)); 27 | // Set meta properties for internal component to make geo bounding box work 28 | dispatch(setInternalValue( 29 | getInternalComponentID(componentId), 30 | undefined, 31 | undefined, 32 | undefined, 33 | meta, 34 | )); 35 | if (forceExecute) { 36 | const executeWatchList = false; 37 | // force execute the map query 38 | const mustExecuteMapQuery = true; 39 | dispatch(executeQuery( 40 | componentId, 41 | executeWatchList, 42 | mustExecuteMapQuery, 43 | componentTypes.reactiveMap, 44 | {}, 45 | queryId, 46 | )); 47 | } 48 | }; 49 | } 50 | 51 | export function setMapResults(componentId, { center, zoom, markers }) { 52 | return { 53 | type: SET_MAP_RESULTS, 54 | componentId, 55 | payload: { 56 | center, 57 | zoom, 58 | markers, 59 | }, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/reducers/aiReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | REMOVE_AI_RESPONSE, 3 | SET_AI_RESPONSE, 4 | SET_AI_RESPONSE_ERROR, 5 | SET_AI_RESPONSE_LOADING, 6 | } from '../constants'; 7 | import { AI_LOCAL_CACHE_KEY } from '../utils/constants'; 8 | import { getObjectFromLocalStorage, setObjectInLocalStorage } from '../utils/helper'; 9 | 10 | export default function aiReducer(state = {}, action) { 11 | if (action.type === SET_AI_RESPONSE) { 12 | setObjectInLocalStorage('AISessions', { 13 | ...(getObjectFromLocalStorage(AI_LOCAL_CACHE_KEY) || {}), 14 | [action.component]: { ...(state[action.component] || {}), ...action.payload }, 15 | }); 16 | return { 17 | ...state, 18 | [action.component]: { 19 | ...(state[action.component] ? state[action.component] : {}), 20 | response: { 21 | ...(state[action.component] 22 | ? state[action.component].response 23 | : {}), 24 | ...action.payload, 25 | }, 26 | isLoading: false, 27 | error: null, 28 | }, 29 | }; 30 | } else if (action.type === REMOVE_AI_RESPONSE) { 31 | const { [action.component]: del, ...obj } = state; 32 | return obj; 33 | } else if (action.type === SET_AI_RESPONSE_ERROR) { 34 | return { 35 | ...state, 36 | [action.component]: { 37 | ...(state[action.component] ? state[action.component] : {}), 38 | error: action.error, 39 | isLoading: false, 40 | response: null, 41 | ...(action.meta || {}), 42 | }, 43 | }; 44 | } else if (action.type === SET_AI_RESPONSE_LOADING) { 45 | return { 46 | ...state, 47 | [action.component]: { 48 | ...(state[action.component] ? state[action.component] : {}), 49 | isLoading: action.isLoading, 50 | }, 51 | }; 52 | } 53 | return state; 54 | } 55 | -------------------------------------------------------------------------------- /src/actions/hits.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_HITS, UPDATE_AGGS, UPDATE_COMPOSITE_AGGS } from '../constants'; 2 | import { SET_QUERY_TO_HITS } from '../../lib/constants'; 3 | import { setAIResponse, setError, setRawData } from './misc'; 4 | 5 | export function updateAggs(component, aggregations, append = false) { 6 | return { 7 | type: UPDATE_AGGS, 8 | component, 9 | aggregations, 10 | append, 11 | }; 12 | } 13 | 14 | export function updateCompositeAggs(component, aggregations, append = false) { 15 | return { 16 | type: UPDATE_COMPOSITE_AGGS, 17 | component, 18 | aggregations, 19 | append, 20 | }; 21 | } 22 | 23 | export function updateHits(component, hits, time, hidden, append = false) { 24 | return { 25 | type: UPDATE_HITS, 26 | component, 27 | hits: hits.hits, 28 | // make compatible with es7 29 | total: typeof hits.total === 'object' ? hits.total.value : hits.total, 30 | hidden, 31 | time, 32 | append, 33 | }; 34 | } 35 | 36 | export function saveQueryToHits(component, query) { 37 | return { 38 | type: SET_QUERY_TO_HITS, 39 | component, 40 | query, 41 | }; 42 | } 43 | 44 | export function mockDataForTesting(component, data) { 45 | return (dispatch) => { 46 | if (data.hasOwnProperty('error')) { 47 | dispatch(setError(component, data.error)); 48 | } 49 | if (data.hasOwnProperty('aggregations')) { 50 | // set aggs 51 | dispatch(updateAggs(component, data.aggregations)); 52 | } 53 | if (data.hasOwnProperty('hits')) { 54 | // set hits 55 | dispatch(updateHits(component, data, data.time || undefined)); 56 | } 57 | if (data.hasOwnProperty('rawData')) { 58 | dispatch(setRawData(component, data.rawData)); 59 | } 60 | if (data.hasOwnProperty('AI_RESPONSE')) { 61 | // set AI response 62 | dispatch(setAIResponse(component, data.AI_RESPONSE)); 63 | // set rawData 64 | } 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@appbaseio/reactivecore", 3 | "version": "10.4.0", 4 | "description": "Core architecture of reactive UI libraries", 5 | "main": "lib/index.js", 6 | "module": "lib/index.mjs", 7 | "files": [ 8 | "lib/" 9 | ], 10 | "scripts": { 11 | "lint": "eslint .", 12 | "start": "rollup -c -w", 13 | "build": "rollup -c", 14 | "precommit": "eslint .", 15 | "prepublishOnly": "yarn run build", 16 | "version-upgrade": "nps upgrade-core -c ../../package-scripts.js", 17 | "test": "../../node_modules/jest/bin/jest.js --watch --env node" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/appbaseio/reactivecore.git" 22 | }, 23 | "author": "Deepak Grover (https://github.com/metagrover)", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/appbaseio/reactivecore/issues" 27 | }, 28 | "homepage": "https://github.com/appbaseio/reactivecore#readme", 29 | "dependencies": { 30 | "cross-fetch": "^3.0.4", 31 | "dayjs": "^1.11.7", 32 | "prop-types": "^15.6.0", 33 | "redux": "^4.0.0", 34 | "redux-thunk": "^2.3.0" 35 | }, 36 | "devDependencies": { 37 | "@rollup/plugin-babel": "^6.0.3", 38 | "@rollup/plugin-commonjs": "^24.1.0", 39 | "@rollup/plugin-json": "^6.0.0", 40 | "@rollup/plugin-node-resolve": "^15.0.2", 41 | "babel-eslint": "^7.2.3", 42 | "babel-preset-react-native": "^4.0.0", 43 | "eslint": "^4.4.0", 44 | "eslint-config-airbnb": "^16.1.0", 45 | "eslint-plugin-babel": "^4.1.2", 46 | "eslint-plugin-import": "^2.8.0", 47 | "eslint-plugin-jest": "^21.12.2", 48 | "eslint-plugin-jsx-a11y": "^6.0.3", 49 | "eslint-plugin-react": "^7.5.1", 50 | "glob": "^10.2.1", 51 | "husky": "^0.14.3", 52 | "jest": "^22.4.2", 53 | "nps": "^5.9.5", 54 | "rollup": "^3.20.7", 55 | "rollup-plugin-babel": "^4.4.0", 56 | "rollup-plugin-terser": "^7.0.2" 57 | }, 58 | "engines": { 59 | "node": ">=10.16.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/polyfills.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // https://tc39.github.io/ecma262/#sec-array.prototype.find 4 | 5 | if (!Array.prototype.find) { 6 | Object.defineProperty(Array.prototype, 'find', { 7 | value: function (predicate) { 8 | // 1. Let O be ? ToObject(this value). 9 | if (this == null) { 10 | throw new TypeError('"this" is null or not defined'); 11 | } 12 | 13 | let o = Object(this); 14 | 15 | // 2. Let len be ? ToLength(? Get(O, "length")). 16 | let len = o.length >>> 0; 17 | 18 | // 3. If IsCallable(predicate) is false, throw a TypeError exception. 19 | if (typeof predicate !== 'function') { 20 | throw new TypeError('predicate must be a function'); 21 | } 22 | 23 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. 24 | let thisArg = arguments[1]; 25 | 26 | // 5. Let k be 0. 27 | let k = 0; 28 | 29 | // 6. Repeat, while k < len 30 | while (k < len) { 31 | // a. Let Pk be ! ToString(k). 32 | // b. Let kValue be ? Get(O, Pk). 33 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)). 34 | // d. If testResult is true, return kValue. 35 | let kValue = o[k]; 36 | if (predicate.call(thisArg, kValue, k, o)) { 37 | return kValue; 38 | } 39 | // e. Increase k by 1. 40 | k++; 41 | } 42 | 43 | // 7. Return undefined. 44 | return undefined; 45 | }, 46 | configurable: true, 47 | writable: true, 48 | }); 49 | } 50 | 51 | if (!String.prototype.endsWith) { 52 | String.prototype.endsWith = function (pattern) { 53 | var d = this.length - pattern.length; 54 | return d >= 0 && this.lastIndexOf(pattern) === d; 55 | }; 56 | } 57 | 58 | if (typeof Event !== 'function') { 59 | function Event(event) { 60 | const evt = document.createEvent('Event'); 61 | evt.initEvent(event, true, true); 62 | return evt; 63 | } 64 | 65 | if (typeof window !== 'undefined') { 66 | window.Event = Event; 67 | } 68 | } 69 | 70 | export default true; 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | import { aiResponseMiddleware } from './utils/middlewares'; 5 | import { STORE_KEY } from './constants'; 6 | import * as Actions from './actions'; 7 | import * as helper from './utils/helper'; 8 | import Suggestions from './utils/suggestions'; 9 | import constants from './utils/constants'; 10 | import polyfills from './utils/polyfills'; 11 | import Causes from './utils/causes'; 12 | import valueReducer from './reducers/valueReducer'; 13 | import queryReducer from './reducers/queryReducer'; 14 | import queryOptionsReducer from './reducers/queryOptionsReducer'; 15 | import dependencyTreeReducer from './reducers/dependencyTreeReducer'; 16 | import propsReducer from './reducers/propsReducer'; 17 | import { defaultAnalyticsConfig } from './utils/analytics'; 18 | 19 | const storeKey = STORE_KEY; 20 | const suggestions = Suggestions; 21 | const causes = Causes; 22 | const Reducers = { 23 | valueReducer, 24 | queryOptionsReducer, 25 | queryReducer, 26 | dependencyTreeReducer, 27 | propsReducer, 28 | }; 29 | export { helper, causes, suggestions, Actions, storeKey, Reducers, constants, polyfills }; 30 | 31 | const composeEnhancers 32 | = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 33 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) 34 | : compose; 35 | 36 | const enhancer = composeEnhancers(applyMiddleware(thunk, aiResponseMiddleware)); 37 | 38 | export default function configureStore(initialState) { 39 | const finalInitialState = { 40 | ...initialState, 41 | config: { 42 | ...initialState.config, 43 | lock: false, 44 | analyticsConfig: 45 | initialState.config && initialState.config.analyticsConfig 46 | ? { 47 | ...defaultAnalyticsConfig, 48 | ...initialState.config.analyticsConfig, 49 | } 50 | : defaultAnalyticsConfig, 51 | }, 52 | }; 53 | return createStore(rootReducer, finalInitialState, enhancer); 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `reactivecore` 2 | 3 | [![npm version](https://badge.fury.io/js/%40appbaseio%2Freactivecore.svg)](https://badge.fury.io/js/%40appbaseio%2Freactivecore) 4 | 5 | This is the platform agnostic core architecture of reactive UI libraries. 6 | 7 | ## Installation 8 | 9 | ``` 10 | yarn add @appbaseio/reactivecore 11 | ``` 12 | 13 | 14 | ## Usage and documentation 15 | 16 | ### Create store: 17 | 18 | ``` 19 | import configureStore from "@appbaseio/reactivecore"; 20 | ``` 21 | 22 | 23 | ### Supported actions: 24 | 25 | Import via: 26 | 27 | ``` 28 | import { } from "@appbaseio/reactivecore/lib/actions" 29 | ``` 30 | 31 | | Action | Usage | 32 | |---------------------------|:------------------------------------------------------| 33 | | `addComponent` | to register a component in the store | 34 | | `removeComponent` | to remove a component from the store | 35 | | `watchComponent` | to set up component subscription | 36 | | `setQuery` | to set the component query in the store | 37 | | `setQueryOptions` | to add external query options | 38 | | `logQuery` | Executed automatically to log query for gatekeeping | 39 | | `executeQuery` | Executed automatically (whenever necessary, based on the dependency tree) when the query of a component is updated| 40 | | `updateHits` | updates results from elasticsearch query | 41 | | `updateQuery` | to update the query in the store - called when a change is triggered in the component| 42 | | `loadMore` | for infinte loading and pagination | 43 | 44 | 45 | ### Utility methods 46 | 47 | Import via: 48 | 49 | ``` 50 | import { } from "@appbaseio/reactivecore/lib/utils" 51 | ``` 52 | 53 | | Method | Usage | 54 | |-----------------------|:----------------------------------------------------------| 55 | | `isEqual` | Compare two objects/arrays | 56 | | `debounce` | Standard debounce | 57 | | `getQueryOptions` | returns applied query options (supports `size` & `from`) | 58 | | `pushToAndClause` | Pushes component to leaf `and` node. Handy for internal component registration | 59 | | `checkValueChange` | checks and executes before/onValueChange for sensors | 60 | | `getAggsOrder` | returns aggs order query based on `sortBy` prop | 61 | | `checkPropChange` | checks for props changes that would need to update the query via callback | 62 | | `checkSomePropChange` | checks for any prop change in the propsList and invokes the callback | 63 | 64 | ## Changelog 65 | 66 | Check the [Changelog](./CHANGELOG.md) doc 67 | -------------------------------------------------------------------------------- /src/reducers/valueReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_VALUE, 3 | RESET_TO_DEFAULT, 4 | CLEAR_VALUES, 5 | REMOVE_COMPONENT, 6 | PATCH_VALUE, 7 | SET_VALUES, 8 | } from '../constants'; 9 | 10 | export default function valueReducer(state = {}, action) { 11 | switch (action.type) { 12 | case SET_VALUE: { 13 | const newState = {}; 14 | Object.keys(action.componentsToReset || {}).forEach((id) => { 15 | newState[id] = { 16 | ...state[id], 17 | value: action.componentsToReset[id], 18 | }; 19 | }); 20 | return { 21 | ...state, 22 | ...newState, 23 | [action.component]: { 24 | value: action.value, 25 | label: action.label || action.component, 26 | showFilter: action.showFilter, 27 | URLParams: action.URLParams, 28 | componentType: action.componentType, 29 | category: action.category, 30 | meta: action.meta, 31 | reference: action.reference, 32 | }, 33 | }; 34 | } 35 | case SET_VALUES: { 36 | const componentKeys = action.componentsValues 37 | ? Object.keys(action.componentsValues) 38 | : []; 39 | if (componentKeys.length) { 40 | const newState = {}; 41 | 42 | componentKeys.forEach((component) => { 43 | const { value, ...rest } = action.componentsValues[component]; 44 | newState[component] = { 45 | ...state[component], 46 | value, 47 | ...rest, 48 | }; 49 | }); 50 | return { ...state, ...newState }; 51 | } 52 | return state; 53 | } 54 | case PATCH_VALUE: 55 | return { 56 | ...state, 57 | [action.component]: { 58 | ...state[action.component], 59 | ...action.payload, 60 | }, 61 | }; 62 | case CLEAR_VALUES: { 63 | const nextState = {}; 64 | if (action.resetValues) { 65 | Object.keys(action.resetValues).forEach((componentId) => { 66 | nextState[componentId] = { 67 | ...state[componentId], 68 | value: action.resetValues[componentId], 69 | }; 70 | }); 71 | } 72 | // clearAllBlacklistComponents has more priority over reset values 73 | if (Array.isArray(action.clearAllBlacklistComponents)) { 74 | Object.keys(state).forEach((componentId) => { 75 | if (action.clearAllBlacklistComponents.includes(componentId)) { 76 | nextState[componentId] = state[componentId]; 77 | } 78 | }); 79 | } 80 | return nextState; 81 | } 82 | case REMOVE_COMPONENT: { 83 | const { [action.component]: del, ...obj } = state; 84 | return obj; 85 | } 86 | case RESET_TO_DEFAULT: 87 | return { 88 | ...state, 89 | ...action.defaultValues, 90 | }; 91 | default: 92 | return state; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const ADD_COMPONENT = 'ADD_COMPONENT'; 2 | export const REMOVE_COMPONENT = 'REMOVE_COMPONENT'; 3 | export const WATCH_COMPONENT = 'WATCH_COMPONENT'; 4 | export const SET_QUERY = 'SET_QUERY'; 5 | export const SET_APPBASE_QUERY = 'SET_APPBASE_QUERY'; 6 | export const SET_QUERY_OPTIONS = 'SET_QUERY_OPTIONS'; 7 | export const EXECUTE_QUERY = 'EXECUTE_QUERY'; 8 | export const UPDATE_HITS = 'UPDATE_HITS'; 9 | export const UPDATE_AGGS = 'UPDATE_AGGS'; 10 | export const UPDATE_COMPOSITE_AGGS = 'UPDATE_COMPOSITE_AGGS'; 11 | export const ADD_CONFIG = 'ADD_CONFIG'; 12 | export const UPDATE_CONFIG = 'UPDATE_CONFIG'; 13 | export const ADD_APPBASE_REF = 'ADD_APPBASE_REF'; 14 | export const ADD_ANALYTICS_REF = 'ADD_ANALYTICS_REF'; 15 | export const LOG_QUERY = 'LOG_QUERY'; 16 | export const LOG_COMBINED_QUERY = 'LOG_COMBINED_QUERY'; 17 | export const SET_INTERNAL_VALUE = 'SET_INTERNAL_VALUE'; 18 | export const PATCH_VALUE = 'PATCH_VALUE'; 19 | export const CLEAR_VALUES = 'CLEAR_VALUES'; 20 | export const SET_LOADING = 'SET_LOADING'; 21 | export const SET_ERROR = 'SET_ERROR'; 22 | export const SET_TIMESTAMP = 'SET_TIMESTAMP'; 23 | export const SET_HEADERS = 'SET_HEADERS'; 24 | export const SET_MAP_DATA = 'SET_MAP_DATA'; 25 | export const SET_MAP_RESULTS = 'SET_MAP_RESULTS'; 26 | export const SET_QUERY_LISTENER = 'SET_QUERY_LISTENER'; 27 | export const STORE_KEY = '__REACTIVESEARCH__'; 28 | export const SET_SEARCH_ID = 'SET_SEARCH_ID'; 29 | export const SET_PROMOTED_RESULTS = 'SET_PROMOTED_RESULTS'; 30 | export const SET_DEFAULT_QUERY = 'SET_DEFAULT_QUERY'; 31 | export const SET_CUSTOM_QUERY = 'SET_CUSTOM_QUERY'; 32 | export const SET_CUSTOM_HIGHLIGHT_OPTIONS = 'SET_CUSTOM_HIGHLIGHT_OPTIONS'; 33 | export const SET_CUSTOM_DATA = 'SET_CUSTOM_DATA'; 34 | export const SET_APPLIED_SETTINGS = 'SET_APPLIED_SETTINGS'; 35 | export const SET_PROPS = 'SET_PROPS'; 36 | export const UPDATE_PROPS = 'UPDATE_PROPS'; 37 | export const REMOVE_PROPS = 'REMOVE_PROPS'; 38 | export const SET_SUGGESTIONS_SEARCH_VALUE = 'SET_SUGGESTIONS_SEARCH_VALUE'; 39 | export const CLEAR_SUGGESTIONS_SEARCH_VALUE = 'CLEAR_SUGGESTIONS_SEARCH_VALUE'; 40 | export const SET_SUGGESTIONS_SEARCH_ID = 'SET_SUGGESTIONS_SEARCH_ID'; 41 | export const UPDATE_ANALYTICS_CONFIG = 'UPDATE_ANALYTICS_CONFIG'; 42 | export const SET_RAW_DATA = 'SET_RAW_DATA'; 43 | export const SET_POPULAR_SUGGESTIONS = 'SET_POPULAR_SUGGESTIONS'; 44 | export const SET_DEFAULT_POPULAR_SUGGESTIONS = 'SET_DEFAULT_POPULAR_SUGGESTIONS'; 45 | export const SET_QUERY_TO_HITS = 'SET_QUERY_TO_HITS'; 46 | export const RECENT_SEARCHES_SUCCESS = 'RECENT_SEARCHES_SUCCESS'; 47 | export const RECENT_SEARCHES_ERROR = 'RECENT_SEARCHES_ERROR'; 48 | export const SET_VALUE = 'SET_VALUE'; 49 | export const SET_VALUES = 'SET_VALUES'; 50 | export const RESET_TO_DEFAULT = 'RESET_TO_DEFAULT'; 51 | export const SET_GOOGLE_MAP_SCRIPT_LOADING = 'SET_GOOGLE_MAP_SCRIPT_LOADING'; 52 | export const SET_GOOGLE_MAP_SCRIPT_LOADED = 'SET_GOOGLE_MAP_SCRIPT_LOADED'; 53 | export const SET_GOOGLE_MAP_SCRIPT_ERROR = 'SET_GOOGLE_MAP_SCRIPT_ERROR'; 54 | export const SET_REGISTERED_COMPONENT_TIMESTAMP = 'SET_REGISTERED_COMPONENT_TIMESTAMP'; 55 | export const REMOVE_REGISTERED_COMPONENT_TIMESTAMP = 'REMOVE_REGISTERED_COMPONENT_TIMESTAMP'; 56 | export const SET_AI_RESPONSE = 'SET_AI_RESPONSE'; 57 | export const SET_AI_RESPONSE_DELAYED = 'SET_AI_RESPONSE_DELAYED'; 58 | export const REMOVE_AI_RESPONSE = 'REMOVE_AI_RESPONSE'; 59 | export const SET_AI_RESPONSE_ERROR = 'SET_AI_RESPONSE_ERROR'; 60 | export const SET_AI_RESPONSE_LOADING = 'SET_AI_RESPONSE_LOADING'; 61 | -------------------------------------------------------------------------------- /src/utils/__tests__/analytics.js: -------------------------------------------------------------------------------- 1 | import getFilterString, { filterComponents, rangeComponents, rangeObjectComponents, parseRangeObject, parseFilterValue } from '../analytics'; 2 | 3 | test('snapshot filter components', () => { 4 | expect(filterComponents).toMatchSnapshot(); 5 | }); 6 | 7 | test('snapshot range components', () => { 8 | expect(rangeComponents).toMatchSnapshot(); 9 | }); 10 | 11 | test('snapshot range object components', () => { 12 | expect(rangeObjectComponents).toMatchSnapshot(); 13 | }); 14 | 15 | test('parseRangeObject should parse the range object', () => { 16 | const filterKey = 'filter'; 17 | const rangeObject = { 18 | start: 1, 19 | end: 3, 20 | }; 21 | const res = parseRangeObject(filterKey, rangeObject); 22 | expect(res).toBe('filter=1~3'); 23 | }); 24 | 25 | test('parseFilterValue should parse string values', () => { 26 | const componentId = 'id1'; 27 | const componentValues = { 28 | label: 'Filter', 29 | value: 'value1', 30 | }; 31 | const res = parseFilterValue(componentId, componentValues); 32 | expect(res).toBe('Filter=value1'); 33 | }); 34 | 35 | test('parseFilterValue should parse string values and fallback to componentId', () => { 36 | const componentId = 'id1'; 37 | const componentValues = { 38 | value: 'value1', 39 | }; 40 | const res = parseFilterValue(componentId, componentValues); 41 | expect(res).toBe('id1=value1'); 42 | }); 43 | 44 | test('parseFilterValue should parse array values', () => { 45 | const componentId = 'id1'; 46 | const componentValues = { 47 | label: 'Filter', 48 | value: ['value1', 'value2'], 49 | }; 50 | const res = parseFilterValue(componentId, componentValues); 51 | expect(res).toBe('Filter=value1,Filter=value2'); 52 | }); 53 | 54 | test('parseFilterValue should parse array of object values', () => { 55 | const componentId = 'id1'; 56 | const componentValues = { 57 | label: 'Filter', 58 | value: [{ label: 'label1', value: 'value1' }], 59 | }; 60 | const res = parseFilterValue(componentId, componentValues); 61 | expect(res).toBe('Filter=value1'); 62 | }); 63 | 64 | test('parseFilterValue should parse array with multiple values', () => { 65 | const componentId = 'id1'; 66 | const componentValues = { 67 | label: 'Filter', 68 | value: [{ label: 'label1', value: 'value1' }, { label: 'label2', value: 'value2' }], 69 | }; 70 | const res = parseFilterValue(componentId, componentValues); 71 | expect(res).toBe('Filter=value1,Filter=value2'); 72 | }); 73 | 74 | test('parseFilterValue should parse range values for DateRange', () => { 75 | const componentId = 'id1'; 76 | const componentValues = { 77 | label: 'Filter', 78 | componentType: 'DATERANGE', 79 | value: ['date1', 'date2'], 80 | }; 81 | const res = parseFilterValue(componentId, componentValues); 82 | expect(res).toBe('Filter=date1~date2'); 83 | }); 84 | 85 | test('getFilterString should return null for falsy value', () => { 86 | const selectedValues = {}; 87 | const res = getFilterString(selectedValues); 88 | expect(res).toBe(null); 89 | }); 90 | 91 | test('getFilterString should return filter string', () => { 92 | const selectedValues = { 93 | filter1: { 94 | value: 'value1', 95 | label: 'Filter1', 96 | componentType: 'SINGLELIST', 97 | }, 98 | filter2: { 99 | value: ['value2', 'value3'], 100 | componentType: 'MULTILIST', 101 | }, 102 | }; 103 | const res = getFilterString(selectedValues); 104 | expect(res).toBe('Filter1=value1,filter2=value2,filter2=value3'); 105 | }); 106 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import componentsReducer from './componentsReducer'; 4 | import watchManReducer from './watchManReducer'; 5 | import dependencyTreeReducer from './dependencyTreeReducer'; 6 | import queryReducer from './queryReducer'; 7 | import appbaseQueryReducer from './appbaseQueryReducer'; 8 | import queryOptionsReducer from './queryOptionsReducer'; 9 | import configReducer from './configReducer'; 10 | import appbaseRefReducer from './appbaseRefReducer'; 11 | import hitsReducer from './hitsReducer'; 12 | import logsReducer from './logsReducer'; 13 | import combinedLogsReducer from './combinedLogsReducer'; 14 | import valueReducer from './valueReducer'; 15 | import internalValueReducer from './internalValueReducer'; 16 | import loadingReducer from './loadingReducer'; 17 | import errorReducer from './errorReducer'; 18 | import timestampReducer from './timestampReducer'; 19 | import headersReducer from './headersReducer'; 20 | import mapDataReducer from './mapDataReducer'; 21 | import queryListenerReducer from './queryListenerReducer'; 22 | import analyticsReducer from './analyticsReducer'; 23 | import promotedResultsReducer from './promotedResultsReducer'; 24 | import customDataReducer from './customDataReducer'; 25 | import defaultQueriesReducer from './defaultQueryReducer'; 26 | import customQueriesReducer from './customQueryReducer'; 27 | import propsReducer from './propsReducer'; 28 | import aggsReducer from './aggsReducer'; 29 | import compositeAggsReducer from './compositeAggsReducer'; 30 | import appliedSettingsReducer from './appliedSettingsReducer'; 31 | import customHighlightReducer from './customHighlightReducer'; 32 | import rawDataReducer from './rawDataReducer'; 33 | import querySuggestionsReducer from './querySuggestionsReducer'; 34 | import defaultPopularSuggestionsReducer from './defaultPopularSuggestions'; 35 | import queryToHitsReducer from './queryToHitsReducer'; 36 | import recentSearchesReducer from './recentSearches'; 37 | import googleMapScriptReducer from './googleMapScriptReducer'; 38 | import analyticsRefReducer from './analyticsRefReducer'; 39 | import registeredComponentReducer from './registeredComponentReducer'; 40 | import aiReducer from './aiReducer'; 41 | 42 | export default combineReducers({ 43 | components: componentsReducer, 44 | customQueries: customQueriesReducer, 45 | defaultQueries: defaultQueriesReducer, 46 | customHighlightOptions: customHighlightReducer, 47 | settings: appliedSettingsReducer, 48 | watchMan: watchManReducer, // contains the list of subscribers 49 | queryList: queryReducer, 50 | queryOptions: queryOptionsReducer, 51 | dependencyTree: dependencyTreeReducer, 52 | appbaseRef: appbaseRefReducer, 53 | analyticsRef: analyticsRefReducer, 54 | config: configReducer, 55 | hits: hitsReducer, 56 | promotedResults: promotedResultsReducer, 57 | customData: customDataReducer, 58 | aggregations: aggsReducer, 59 | compositeAggregations: compositeAggsReducer, 60 | queryLog: logsReducer, 61 | combinedLog: combinedLogsReducer, 62 | selectedValues: valueReducer, 63 | internalValues: internalValueReducer, 64 | isLoading: loadingReducer, 65 | error: errorReducer, 66 | timestamp: timestampReducer, 67 | headers: headersReducer, 68 | mapData: mapDataReducer, // holds the map id and boolean to execute geo-bound-query 69 | queryListener: queryListenerReducer, 70 | analytics: analyticsReducer, 71 | props: propsReducer, 72 | rawData: rawDataReducer, 73 | querySuggestions: querySuggestionsReducer, 74 | defaultPopularSuggestions: defaultPopularSuggestionsReducer, 75 | queryToHits: queryToHitsReducer, // holds the query value for last fetched `hits` 76 | recentSearches: recentSearchesReducer, 77 | urlValues: (state = {}) => state, 78 | googleMapScriptStatus: googleMapScriptReducer, 79 | lastUsedAppbaseQuery: appbaseQueryReducer, 80 | registeredComponentsTimestamps: registeredComponentReducer, 81 | AIResponses: aiReducer, 82 | }); 83 | -------------------------------------------------------------------------------- /src/utils/analytics.js: -------------------------------------------------------------------------------- 1 | import { componentTypes } from '../utils/constants'; 2 | 3 | const filterComponents = [ 4 | componentTypes.numberBox, 5 | componentTypes.tagCloud, 6 | componentTypes.toggleButton, 7 | componentTypes.datePicker, 8 | componentTypes.dateRange, 9 | componentTypes.multiDataList, 10 | componentTypes.multiDropdownList, 11 | componentTypes.multiList, 12 | componentTypes.singleDataList, 13 | componentTypes.singleDropdownList, 14 | componentTypes.singleList, 15 | componentTypes.dynamicRangeSlider, 16 | componentTypes.multiDropdownRange, 17 | componentTypes.multiRange, 18 | componentTypes.rangeSlider, 19 | componentTypes.ratingsFilter, 20 | componentTypes.singleDropdownRange, 21 | componentTypes.singleRange, 22 | componentTypes.treeList, 23 | ]; 24 | 25 | // components storing range as array 26 | const rangeComponents = [ 27 | componentTypes.dateRange, 28 | componentTypes.dynamicRangeSlider, 29 | componentTypes.rangeSlider, 30 | componentTypes.rangeInput, 31 | componentTypes.ratingsFilter, 32 | ]; 33 | 34 | // components storing range as object 35 | const rangeObjectComponents = [ 36 | componentTypes.singleRange, 37 | componentTypes.singleDropdownRange, 38 | componentTypes.multiRange, 39 | componentTypes.multiDropdownRange, 40 | ]; 41 | 42 | function parseRangeObject(filterKey, rangeObject) { 43 | return `${filterKey}=${rangeObject.start}~${rangeObject.end}`; 44 | } 45 | 46 | function parseFilterValue(componentId, componentValues) { 47 | const { label, value, componentType } = componentValues; 48 | const filterKey = label || componentId; 49 | if (rangeComponents.includes(componentType)) { 50 | // range components store values as an array to depict start and end range 51 | return `${filterKey}=${value[0]}~${value[1]}`; 52 | } else if (rangeObjectComponents.includes(componentType)) { 53 | // for range components with values in the form { start, end } 54 | if (Array.isArray(value)) { 55 | return value.map(item => parseRangeObject(filterKey, item)).join(); 56 | } 57 | return parseRangeObject(filterKey, value); 58 | } else if (Array.isArray(value)) { 59 | // for components having values in the form { label, value } 60 | const isObject = typeof value[0] === 'object' && value[0] !== null; 61 | return isObject 62 | ? value.map(item => `${filterKey}=${item.value}`).join() 63 | : value.map(item => `${filterKey}=${item}`).join(); 64 | } 65 | return `${filterKey}=${value}`; 66 | } 67 | 68 | // transforms the selectedValues from store into the X-Search-Filters string for analytics 69 | function getFilterString(selectedValues) { 70 | if (selectedValues && Object.keys(selectedValues).length) { 71 | return ( 72 | Object 73 | // take all selectedValues 74 | .entries(selectedValues) 75 | // filter out filter components having some value 76 | .filter(([, componentValues]) => 77 | filterComponents.includes(componentValues.componentType) 78 | // in case of an array filter out empty array values as well 79 | && ((componentValues.value && componentValues.value.length) 80 | // also consider range values in the shape { start, end } 81 | || (componentValues.value && componentValues.value.start) 82 | || (componentValues.value && componentValues.value.end))) 83 | // parse each filter value 84 | .map(([componentId, componentValues]) => 85 | parseFilterValue(componentId, componentValues)) 86 | // return as a string separated with comma 87 | .join() 88 | ); 89 | } 90 | return null; 91 | } 92 | 93 | /** 94 | * Function to parse the custom analytics events 95 | */ 96 | function parseCustomEvents(customEvents) { 97 | let finalStr = ''; 98 | Object.keys(customEvents).forEach((key, index) => { 99 | finalStr += `${key}=${customEvents[key]}`; 100 | if (index < Object.keys(customEvents).length - 1) { 101 | finalStr += ','; 102 | } 103 | }); 104 | return finalStr; 105 | } 106 | 107 | // defaultAnalytics Config 108 | export const defaultAnalyticsConfig = { 109 | emptyQuery: true, 110 | suggestionAnalytics: true, 111 | userId: null, 112 | customEvents: null, 113 | enableQueryRules: true, 114 | }; 115 | 116 | export { 117 | filterComponents, 118 | rangeComponents, 119 | rangeObjectComponents, 120 | parseFilterValue, 121 | parseRangeObject, 122 | parseCustomEvents, 123 | }; 124 | export default getFilterString; 125 | -------------------------------------------------------------------------------- /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const componentTypes = { 2 | reactiveList: 'REACTIVELIST', 3 | // search components 4 | dataSearch: 'DATASEARCH', 5 | categorySearch: 'CATEGORYSEARCH', 6 | searchBox: 'SEARCHBOX', 7 | // list components 8 | singleList: 'SINGLELIST', 9 | multiList: 'MULTILIST', 10 | singleDataList: 'SINGLEDATALIST', 11 | tabDataList: 'TABDATALIST', 12 | singleDropdownList: 'SINGLEDROPDOWNLIST', 13 | multiDataList: 'MULTIDATALIST', 14 | multiDropdownList: 'MULTIDROPDOWNLIST', 15 | singleDropdownRange: 'SINGLEDROPDOWNRANGE', 16 | treeList: 'TREELIST', 17 | // basic components 18 | numberBox: 'NUMBERBOX', 19 | tagCloud: 'TAGCLOUD', 20 | toggleButton: 'TOGGLEBUTTON', 21 | reactiveComponent: 'REACTIVECOMPONENT', 22 | // range components 23 | datePicker: 'DATEPICKER', 24 | dateRange: 'DATERANGE', 25 | dynamicRangeSlider: 'DYNAMICRANGESLIDER', 26 | multiDropdownRange: 'MULTIDROPDOWNRANGE', 27 | singleRange: 'SINGLERANGE', 28 | multiRange: 'MULTIRANGE', 29 | rangeSlider: 'RANGESLIDER', 30 | ratingsFilter: 'RATINGSFILTER', 31 | rangeInput: 'RANGEINPUT', 32 | // map components 33 | geoDistanceDropdown: 'GEO_DISTANCE_DROPDOWN', 34 | geoDistanceSlider: 'GEO_DISTANCE_SLIDER', 35 | reactiveMap: 'REACTIVE_MAP', 36 | // chart components 37 | reactiveChart: 'REACTIVE_CHART', 38 | // ai component 39 | AIAnswer: 'AI_ANSWER', 40 | }; 41 | 42 | export const queryTypes = { 43 | search: 'search', 44 | term: 'term', 45 | range: 'range', 46 | geo: 'geo', 47 | suggestion: 'suggestion', 48 | }; 49 | 50 | // Props that need to be passed to the query 51 | export const validProps = [ 52 | // common 53 | 'type', 54 | 'componentType', 55 | 'aggregationField', 56 | 'aggregationSize', 57 | 'distinctField', 58 | 'distinctFieldConfig', 59 | 'index', 60 | 'aggregations', 61 | 'compoundClause', 62 | // Specific to ReactiveList 63 | 'dataField', 64 | 'includeFields', 65 | 'excludeFields', 66 | 'size', 67 | 'candidates', 68 | 'from', 69 | 'sortBy', 70 | 'sortOptions', 71 | 'pagination', 72 | // Specific to DataSearch 73 | 'autoFocus', 74 | 'autosuggest', 75 | 'debounce', 76 | 'defaultValue', 77 | 'defaultSuggestions', 78 | 'fieldWeights', 79 | 'filterLabel', 80 | 'fuzziness', 81 | 'highlight', 82 | 'highlightConfig', 83 | 'highlightField', 84 | 'nestedField', 85 | 'placeholder', 86 | 'queryFormat', 87 | 'searchOperators', 88 | 'enableSynonyms', 89 | 'enableQuerySuggestions', 90 | 'queryString', 91 | 'vectorDataField', 92 | 'imageValue', 93 | // Specific to Category Search 94 | 'categoryField', 95 | 'strictSelection', 96 | // Specific to List Components 97 | 'selectAllLabel', 98 | 'showCheckbox', 99 | 'showFilter', 100 | 'showSearch', 101 | 'showCount', 102 | 'showLoadMore', 103 | 'loadMoreLabel', 104 | 'showMissing', 105 | 'missingLabel', 106 | 'data', 107 | 'showRadio', 108 | // TagCloud and ToggleButton 109 | 'multiSelect', 110 | // Range Components 111 | 'includeNullValues', 112 | 'interval', 113 | 'showHistogram', 114 | 'snap', 115 | 'stepValue', 116 | 'range', 117 | 'showSlider', 118 | 'parseDate', 119 | 'calendarInterval', 120 | // Map components 121 | 'unit', 122 | // Specific to SearchBox 123 | 'enablePopularSuggestions', 124 | 'enableRecentSuggestions', 125 | 'popularSuggestionsConfig', 126 | 'recentSuggestionsConfig', 127 | 'indexSuggestionsConfig', 128 | 'featuredSuggestionsConfig', 129 | 'FAQSuggestionsConfig', 130 | 'documentSuggestionsConfig', 131 | 'enablePredictiveSuggestions', 132 | 'applyStopwords', 133 | 'customStopwords', 134 | 'enableIndexSuggestions', 135 | 'enableFeaturedSuggestions', 136 | 'enableFAQSuggestions', 137 | 'enableDocumentSuggestions', 138 | 'enableEndpointSuggestions', 139 | 'searchboxId', 140 | 'endpoint', 141 | 'enableAI', 142 | 'AIConfig', 143 | 'showDistinctSuggestions', 144 | ]; 145 | 146 | export const CLEAR_ALL = { 147 | NEVER: 'never', 148 | ALWAYS: 'always', 149 | DEFAULT: 'default', 150 | }; 151 | 152 | // search components modes 153 | export const SEARCH_COMPONENTS_MODES = { 154 | SELECT: 'select', 155 | TAG: 'tag', 156 | }; 157 | 158 | export const TREELIST_VALUES_PATH_SEPARATOR = '◐◑◒◓'; 159 | 160 | export const AI_ROLES = { 161 | USER: 'user', 162 | SYSTEM: 'system', 163 | ASSISTANT: 'assistant', 164 | }; 165 | 166 | export const AI_LOCAL_CACHE_KEY = 'AISessions'; 167 | 168 | export const AI_TRIGGER_MODES = { 169 | QUESTION: 'question', 170 | MANUAL: 'manual', 171 | ALWAYS: 'always', 172 | }; 173 | export default true; 174 | -------------------------------------------------------------------------------- /src/utils/types.js: -------------------------------------------------------------------------------- 1 | import { 2 | oneOfType, 3 | string, 4 | arrayOf, 5 | object, 6 | func, 7 | any, 8 | bool, 9 | oneOf, 10 | shape, 11 | number, 12 | array, 13 | } from 'prop-types'; 14 | 15 | import dateFormats from './dateFormats'; 16 | import { CLEAR_ALL, componentTypes } from './constants'; 17 | 18 | const reactKeyType = oneOfType([string, arrayOf(string), object, arrayOf(object)]); 19 | 20 | function validateLocation(props, propName) { 21 | // eslint-disable-next-line 22 | if (isNaN(props[propName])) { 23 | return new Error(`${propName} value must be a number`); 24 | } 25 | if (propName === 'lat' && (props[propName] < -90 || props[propName] > 90)) { 26 | return new Error(`${propName} value should be between -90 and 90.`); 27 | } else if (propName === 'lng' && (props[propName] < -180 || props[propName] > 180)) { 28 | return new Error(`${propName} value should be between -180 and 180.`); 29 | } 30 | return null; 31 | } 32 | 33 | // eslint-disable-next-line consistent-return 34 | const dataFieldValidator = (props, propName, componentName) => { 35 | const requiredError = new Error(`${propName} supplied to ${componentName} is required. Validation failed.`); 36 | const propValue = props[propName]; 37 | if (!propValue) return requiredError; 38 | if ( 39 | typeof propValue !== 'string' 40 | && typeof propValue !== 'object' 41 | && !Array.isArray(propValue) 42 | ) { 43 | return new Error(`Invalid ${propName} supplied to ${componentName}. Validation failed.`); 44 | } 45 | if (Array.isArray(propValue) && propValue.length === 0) return requiredError; 46 | }; 47 | 48 | const types = { 49 | any, 50 | analyticsConfig: shape({ 51 | emptyQuery: bool, 52 | suggestionAnalytics: bool, 53 | userId: string, 54 | customEvents: object, // eslint-disable-line 55 | }), 56 | appbaseConfig: shape({ 57 | enableQueryRules: bool, 58 | enableSearchRelevancy: bool, 59 | recordAnalytics: bool, 60 | emptyQuery: bool, 61 | suggestionAnalytics: bool, 62 | userId: string, 63 | useCache: bool, 64 | customEvents: object, // eslint-disable-line 65 | enableTelemetry: bool, 66 | queryString: object, // eslint-disable-line 67 | }), 68 | bool, 69 | boolRequired: bool.isRequired, 70 | components: arrayOf(string), 71 | compoundClause: oneOf(['filter', 'must']), 72 | children: any, 73 | data: arrayOf(object), 74 | dataFieldArray: oneOfType([string, arrayOf(string)]).isRequired, 75 | dataNumberBox: shape({ 76 | label: string, 77 | start: number.isRequired, 78 | end: number.isRequired, 79 | }).isRequired, 80 | date: oneOfType([string, arrayOf(string)]), 81 | dateObject: object, 82 | excludeFields: arrayOf(string), 83 | fieldWeights: arrayOf(number), 84 | filterLabel: string, 85 | func, 86 | funcRequired: func.isRequired, 87 | fuzziness: oneOf([0, 1, 2, 'AUTO']), 88 | headers: object, 89 | hits: arrayOf(object), 90 | rawData: object, 91 | iconPosition: oneOf(['left', 'right']), 92 | includeFields: arrayOf(string), 93 | labelPosition: oneOf(['left', 'right', 'top', 'bottom']), 94 | number, 95 | options: oneOfType([arrayOf(object), object]), 96 | paginationAt: oneOf(['top', 'bottom', 'both']), 97 | range: shape({ 98 | start: oneOfType([number, string, object]).isRequired, 99 | end: oneOfType([number, string, object]).isRequired, 100 | }), 101 | rangeLabels: shape({ 102 | start: string.isRequired, 103 | end: string.isRequired, 104 | }), 105 | react: shape({ 106 | and: reactKeyType, 107 | or: reactKeyType, 108 | not: reactKeyType, 109 | }), 110 | categorySearchValue: shape({ 111 | term: string, 112 | category: string, 113 | }), 114 | selectedValues: object, 115 | selectedValue: oneOfType([ 116 | string, 117 | arrayOf(string), 118 | arrayOf(object), 119 | object, 120 | number, 121 | arrayOf(number), 122 | ]), 123 | suggestions: arrayOf(object), 124 | supportedOrientations: oneOf([ 125 | 'portrait', 126 | 'portrait-upside-down', 127 | 'landscape', 128 | 'landscape-left', 129 | 'landscape-right', 130 | ]), 131 | tooltipTrigger: oneOf(['hover', 'none', 'focus', 'always']), 132 | sortBy: oneOf(['asc', 'desc']), 133 | sortOptions: arrayOf(shape({ 134 | label: string, 135 | dataField: string, 136 | sortBy: string, 137 | })), 138 | sortByWithCount: oneOf(['asc', 'desc', 'count']), 139 | stats: arrayOf(object), 140 | string, 141 | stringArray: arrayOf(string), 142 | stringOrArray: oneOfType([string, arrayOf(string)]), 143 | stringRequired: string.isRequired, 144 | style: object, 145 | themePreset: oneOf(['light', 'dark']), 146 | queryFormatDate: oneOf(Object.keys(dateFormats)), 147 | queryFormatSearch: oneOf(['and', 'or']), 148 | queryFormatNumberBox: oneOf(['exact', 'lte', 'gte']), 149 | params: object.isRequired, 150 | props: object, 151 | rangeLabelsAlign: oneOf(['left', 'right']), 152 | title: oneOfType([string, any]), 153 | location: shape({ 154 | lat: validateLocation, 155 | lng: validateLocation, 156 | }), 157 | unit: oneOf([ 158 | 'mi', 159 | 'miles', 160 | 'yd', 161 | 'yards', 162 | 'ft', 163 | 'feet', 164 | 'in', 165 | 'inch', 166 | 'km', 167 | 'kilometers', 168 | 'm', 169 | 'meters', 170 | 'cm', 171 | 'centimeters', 172 | 'mm', 173 | 'millimeters', 174 | 'NM', 175 | 'nmi', 176 | 'nauticalmiles', 177 | ]), 178 | aggregationData: array, 179 | showClearAll: oneOf([CLEAR_ALL.NEVER, CLEAR_ALL.ALWAYS, CLEAR_ALL.DEFAULT, true, false]), 180 | componentType: oneOf(Object.values(componentTypes)), 181 | componentObject: object, 182 | dataFieldValidator, 183 | focusShortcuts: oneOfType([arrayOf(string), arrayOf(number)]), 184 | mongodb: shape({ 185 | db: string, 186 | collection: string, 187 | }), 188 | calendarInterval: oneOf(['month', 'day', 'year', 'week', 'quarter', 'hour', 'minute']), 189 | preferences: object, 190 | endpoint: shape({ 191 | url: string.isRequired, 192 | method: string, 193 | /* eslint-disable react/forbid-prop-types */ 194 | headers: object, 195 | body: object, 196 | /* eslint-enable react/forbid-prop-types */ 197 | }), 198 | AIConfig: shape({ 199 | docTemplate: string, 200 | queryTemplate: string, 201 | maxTokens: number, 202 | systemPrompt: string, 203 | temperature: number, 204 | topDocsForContext: number, 205 | }), 206 | }; 207 | 208 | export default types; 209 | -------------------------------------------------------------------------------- /src/actions/value.js: -------------------------------------------------------------------------------- 1 | import { componentTypes } from '../utils/constants'; 2 | import { isEqual } from '../utils/helper'; 3 | import { 4 | SET_VALUE, 5 | CLEAR_VALUES, 6 | PATCH_VALUE, 7 | SET_INTERNAL_VALUE, 8 | RESET_TO_DEFAULT, 9 | SET_VALUES, 10 | } from '../constants'; 11 | import { updateStoreConfig } from './utils'; 12 | 13 | export function setValue( 14 | component, 15 | value, 16 | label, 17 | showFilter, 18 | URLParams, 19 | componentType, 20 | category, 21 | meta, 22 | updateSource, // valid values => 'URL' 23 | ) { 24 | return (dispatch, getState) => { 25 | const { 26 | urlValues, selectedValues, watchMan, props, 27 | } = getState(); 28 | // set the value reference 29 | let reference = updateSource; 30 | if (isEqual(urlValues[component], value)) { 31 | reference = 'URL'; 32 | } 33 | // Clear pagination state for result components 34 | // Only clear when value is not set by URL params 35 | const componentsToReset = {}; 36 | const isResultComponent = [ 37 | componentTypes.reactiveList, 38 | componentTypes.reactiveMap, 39 | ].includes(props[component] && props[component].componentType); 40 | const previousValue = selectedValues[component] && selectedValues[component].value; 41 | // Incase of searchbox .meta.imageValue can be modified although .value is not modified. 42 | let isImageValueEqual = true; 43 | // SelectedFilters may not have componentType but they need to clear the value 44 | if (componentType === componentTypes.searchBox || componentType === undefined) { 45 | const previousImageValue = selectedValues[component] && selectedValues[component].meta 46 | && selectedValues[component].meta.imageValue; 47 | isImageValueEqual = isEqual(previousImageValue, meta && meta.imageValue); 48 | } 49 | if ((!isEqual(previousValue, value) || !isImageValueEqual) 50 | && props[component] && !isResultComponent) { 51 | let componentList = [component]; 52 | const watchList = watchMan[component] || []; 53 | componentList = [...componentList, ...watchList]; 54 | componentList.forEach((comp) => { 55 | // Clear pagination state for result components 56 | // Only clear when value is not set by URL params 57 | const componentProps = props[comp]; 58 | if ( 59 | reference !== 'URL' 60 | && componentProps 61 | // eslint-disable-next-line max-len 62 | && [componentTypes.reactiveList, componentTypes.reactiveMap].includes(componentProps.componentType) 63 | ) { 64 | if (selectedValues[comp] !== null) { 65 | componentsToReset[comp] = null; 66 | } 67 | } 68 | }); 69 | } 70 | if (isResultComponent) { 71 | // reject default page requests 72 | if (value < 2 && (!previousValue || previousValue < 2)) { 73 | return; 74 | } 75 | } 76 | dispatch({ 77 | type: SET_VALUE, 78 | component, 79 | reference, 80 | value, 81 | label, 82 | showFilter, 83 | URLParams, 84 | componentType, 85 | category, 86 | meta, 87 | componentsToReset, 88 | }); 89 | }; 90 | } 91 | 92 | export function resetValuesToDefault(clearAllBlacklistComponents) { 93 | return (dispatch, getState) => { 94 | const { selectedValues, props: componentProps } = getState(); 95 | let defaultValues = { 96 | // componentName: defaultValue, 97 | }; 98 | 99 | let valueToSet; 100 | Object.keys(selectedValues).forEach((component) => { 101 | if ( 102 | !( 103 | Array.isArray(clearAllBlacklistComponents) 104 | && clearAllBlacklistComponents.includes(component) 105 | ) 106 | ) { 107 | if ( 108 | !componentProps[component] 109 | || !componentProps[component].componentType 110 | || !componentProps[component].defaultValue 111 | ) { 112 | valueToSet = null; 113 | } else if ( 114 | [ 115 | componentTypes.rangeSlider, 116 | componentTypes.rangeInput, 117 | componentTypes.ratingsFilter, 118 | componentTypes.dateRange, 119 | ].includes(componentProps[component].componentType) 120 | ) { 121 | valueToSet 122 | = typeof componentProps[component].defaultValue === 'object' 123 | ? [ 124 | componentProps[component].defaultValue.start, 125 | componentProps[component].defaultValue.end, 126 | ] 127 | : null; 128 | } else if ( 129 | [ 130 | componentTypes.multiDropdownList, 131 | componentTypes.multiDataList, 132 | componentTypes.multiList, 133 | componentTypes.singleDataList, 134 | componentTypes.singleDropdownList, 135 | componentTypes.singleList, 136 | componentTypes.tagCloud, 137 | componentTypes.toggleButton, 138 | componentTypes.multiDropdownRange, 139 | componentTypes.multiRange, 140 | componentTypes.singleDropdownRange, 141 | componentTypes.singleRange, 142 | componentTypes.dataSearch, 143 | componentTypes.datePicker, 144 | componentTypes.treeList, 145 | ].includes(componentProps[component].componentType) 146 | ) { 147 | valueToSet = componentProps[component].defaultValue; 148 | } else if ( 149 | [componentTypes.categorySearch].includes(componentProps[component].componentType) 150 | ) { 151 | valueToSet = componentProps[component].defaultValue 152 | ? componentProps[component].defaultValue.term 153 | : ''; 154 | } 155 | if (!isEqual(selectedValues[component].value, valueToSet)) { 156 | defaultValues = { 157 | ...defaultValues, 158 | [component]: { 159 | ...selectedValues[component], 160 | value: valueToSet, 161 | }, 162 | }; 163 | } 164 | } 165 | }); 166 | dispatch({ 167 | type: RESET_TO_DEFAULT, 168 | defaultValues, 169 | }); 170 | }; 171 | } 172 | export function setInternalValue(component, value, componentType, category, meta) { 173 | return { 174 | type: SET_INTERNAL_VALUE, 175 | component, 176 | value, 177 | componentType, 178 | category, 179 | meta, 180 | }; 181 | } 182 | /** 183 | * Patches the properties of the component 184 | * @param {String} component 185 | * @param {Object} payload 186 | */ 187 | export function patchValue(component, payload) { 188 | return { 189 | type: PATCH_VALUE, 190 | component, 191 | payload, 192 | }; 193 | } 194 | export function clearValues(resetValues = {}, clearAllBlacklistComponents = []) { 195 | return { 196 | type: CLEAR_VALUES, 197 | resetValues, 198 | clearAllBlacklistComponents, 199 | }; 200 | } 201 | 202 | export function setValues(componentsValues) { 203 | return (dispatch) => { 204 | dispatch(updateStoreConfig({ 205 | queryLockConfig: { initialTimestamp: new Date().getTime(), lockTime: 300 }, 206 | })); 207 | dispatch({ 208 | type: SET_VALUES, 209 | componentsValues, 210 | }); 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /src/actions/misc.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_QUERY, 3 | SET_QUERY_OPTIONS, 4 | LOG_QUERY, 5 | LOG_COMBINED_QUERY, 6 | SET_LOADING, 7 | SET_TIMESTAMP, 8 | SET_HEADERS, 9 | SET_QUERY_LISTENER, 10 | SET_SEARCH_ID, 11 | SET_ERROR, 12 | SET_PROMOTED_RESULTS, 13 | SET_APPLIED_SETTINGS, 14 | SET_SUGGESTIONS_SEARCH_ID, 15 | SET_CUSTOM_DATA, 16 | SET_DEFAULT_QUERY, 17 | SET_CUSTOM_HIGHLIGHT_OPTIONS, 18 | SET_CUSTOM_QUERY, 19 | SET_POPULAR_SUGGESTIONS, 20 | SET_RAW_DATA, 21 | SET_DEFAULT_POPULAR_SUGGESTIONS, 22 | SET_GOOGLE_MAP_SCRIPT_LOADING, 23 | SET_GOOGLE_MAP_SCRIPT_LOADED, 24 | SET_GOOGLE_MAP_SCRIPT_ERROR, 25 | SET_APPBASE_QUERY, 26 | SET_AI_RESPONSE, 27 | REMOVE_AI_RESPONSE, 28 | SET_AI_RESPONSE_ERROR, 29 | SET_AI_RESPONSE_LOADING, 30 | SET_AI_RESPONSE_DELAYED, 31 | } from '../constants'; 32 | 33 | import { transformValueToComponentStateFormat } from '../utils/transform'; 34 | import { updateAggs, updateCompositeAggs, updateHits } from './hits'; 35 | import { setValues } from './value'; 36 | 37 | export function setRawData(component, response) { 38 | return { 39 | type: SET_RAW_DATA, 40 | component, 41 | response, 42 | }; 43 | } 44 | 45 | export function setLoading(component, isLoading) { 46 | return { 47 | type: SET_LOADING, 48 | component, 49 | isLoading, 50 | }; 51 | } 52 | 53 | export function setError(component, error) { 54 | return { 55 | type: SET_ERROR, 56 | component, 57 | error, 58 | }; 59 | } 60 | 61 | export function setTimestamp(component, timestamp) { 62 | return { 63 | type: SET_TIMESTAMP, 64 | component, 65 | timestamp, 66 | }; 67 | } 68 | 69 | export function setSearchId(searchId = null) { 70 | return { 71 | type: SET_SEARCH_ID, 72 | searchId, 73 | }; 74 | } 75 | 76 | export function setSuggestionsSearchId(searchId = null) { 77 | return { 78 | type: SET_SUGGESTIONS_SEARCH_ID, 79 | searchId, 80 | }; 81 | } 82 | 83 | export function setQuery(component, query) { 84 | return { 85 | type: SET_QUERY, 86 | component, 87 | query, 88 | }; 89 | } 90 | 91 | export function setCustomQuery(component, query) { 92 | return { 93 | type: SET_CUSTOM_QUERY, 94 | component, 95 | query, 96 | }; 97 | } 98 | 99 | export function setDefaultQuery(component, query) { 100 | return { 101 | type: SET_DEFAULT_QUERY, 102 | component, 103 | query, 104 | }; 105 | } 106 | 107 | export function setCustomHighlightOptions(component, data) { 108 | return { 109 | type: SET_CUSTOM_HIGHLIGHT_OPTIONS, 110 | component, 111 | data, 112 | }; 113 | } 114 | 115 | export function updateQueryOptions(component, options) { 116 | return { 117 | type: SET_QUERY_OPTIONS, 118 | component, 119 | options, 120 | }; 121 | } 122 | 123 | // gatekeeping for normal queries 124 | export function logQuery(component, query) { 125 | return { 126 | type: LOG_QUERY, 127 | component, 128 | query, 129 | }; 130 | } 131 | 132 | // gatekeeping for queries combined with map queries 133 | export function logCombinedQuery(component, query) { 134 | return { 135 | type: LOG_COMBINED_QUERY, 136 | component, 137 | query, 138 | }; 139 | } 140 | 141 | export function setHeaders(headers) { 142 | return { 143 | type: SET_HEADERS, 144 | headers, 145 | }; 146 | } 147 | 148 | export function setPromotedResults(results = [], component) { 149 | return { 150 | type: SET_PROMOTED_RESULTS, 151 | results, 152 | component, 153 | }; 154 | } 155 | 156 | export function setPopularSuggestions(suggestions = [], component) { 157 | return { 158 | type: SET_POPULAR_SUGGESTIONS, 159 | suggestions, 160 | component, 161 | }; 162 | } 163 | 164 | export function setDefaultPopularSuggestions(suggestions = [], component) { 165 | return { 166 | type: SET_DEFAULT_POPULAR_SUGGESTIONS, 167 | suggestions, 168 | component, 169 | }; 170 | } 171 | 172 | export function setCustomData(data = null, component) { 173 | return { 174 | type: SET_CUSTOM_DATA, 175 | data, 176 | component, 177 | }; 178 | } 179 | 180 | export function setAppliedSettings(data = null, component) { 181 | return { 182 | type: SET_APPLIED_SETTINGS, 183 | data, 184 | component, 185 | }; 186 | } 187 | 188 | export function setQueryListener(component, onQueryChange, onError) { 189 | return { 190 | type: SET_QUERY_LISTENER, 191 | component, 192 | onQueryChange, 193 | onError, 194 | }; 195 | } 196 | 197 | export function setGoogleMapScriptLoading(bool) { 198 | return { type: SET_GOOGLE_MAP_SCRIPT_LOADING, loading: bool }; 199 | } 200 | export function setGoogleMapScriptLoaded(bool) { 201 | return { type: SET_GOOGLE_MAP_SCRIPT_LOADED, loaded: bool }; 202 | } 203 | export function setGoogleMapScriptError(error) { 204 | return { type: SET_GOOGLE_MAP_SCRIPT_ERROR, error }; 205 | } 206 | 207 | export function resetStoreForComponent(componentId) { 208 | return (dispatch) => { 209 | dispatch(setRawData(componentId, null)); 210 | dispatch(setCustomData(null, componentId)); 211 | dispatch(setPromotedResults([], componentId)); 212 | dispatch(setPopularSuggestions([], componentId)); 213 | dispatch(setDefaultPopularSuggestions([], componentId)); 214 | dispatch(updateAggs(componentId, null)); 215 | dispatch(updateCompositeAggs(componentId, {})); 216 | dispatch(updateHits(componentId, { hits: [], total: 0 }, 0)); 217 | }; 218 | } 219 | 220 | export function setLastUsedAppbaseQuery(query) { 221 | return { 222 | type: SET_APPBASE_QUERY, 223 | query, 224 | }; 225 | } 226 | 227 | export function setSearchState(componentsValueAndTypeMap = {}) { 228 | return (dispatch) => { 229 | const componentValues = {}; 230 | Object.keys(componentsValueAndTypeMap).forEach((componentId) => { 231 | const { value, componentProps } = componentsValueAndTypeMap[componentId]; 232 | const { value: transformedValue, meta = {} } = transformValueToComponentStateFormat( 233 | value, 234 | componentProps, 235 | ); 236 | componentValues[componentId] = { 237 | value: transformedValue, 238 | ...meta, 239 | }; 240 | }); 241 | 242 | dispatch(setValues(componentValues)); 243 | }; 244 | } 245 | 246 | export function setAIResponse(component, payload) { 247 | return { 248 | type: SET_AI_RESPONSE, 249 | component, 250 | payload, 251 | }; 252 | } 253 | 254 | export function setAIResponseDelayed(component, payload) { 255 | return { 256 | type: SET_AI_RESPONSE_DELAYED, 257 | component, 258 | payload, 259 | }; 260 | } 261 | 262 | export function removeAIResponse(component) { 263 | return { 264 | type: REMOVE_AI_RESPONSE, 265 | component, 266 | }; 267 | } 268 | 269 | export function setAIResponseError(component, error, meta = {}) { 270 | return { 271 | type: SET_AI_RESPONSE_ERROR, 272 | component, 273 | error, 274 | meta, 275 | }; 276 | } 277 | 278 | export function setAIResponseLoading(component, isLoading) { 279 | return { 280 | type: SET_AI_RESPONSE_LOADING, 281 | component, 282 | isLoading, 283 | }; 284 | } 285 | -------------------------------------------------------------------------------- /src/utils/suggestions.js: -------------------------------------------------------------------------------- 1 | import diacritics from './diacritics'; 2 | 3 | // flattens a nested array 4 | const flatten = arr => 5 | arr.reduce( 6 | (flat, toFlatten) => flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten), 7 | [], 8 | ); 9 | 10 | const extractSuggestion = (val) => { 11 | switch (typeof val) { 12 | case 'string': 13 | return val; 14 | case 'object': 15 | if (Array.isArray(val)) { 16 | return flatten(val); 17 | } 18 | return null; 19 | 20 | default: 21 | return val; 22 | } 23 | }; 24 | 25 | export function replaceDiacritics(s) { 26 | let str = s ? String(s) : ''; 27 | 28 | for (let i = 0; i < str.length; i++) { 29 | const currentChar = str.charAt(i); 30 | if (diacritics[currentChar]) { 31 | str = str.replaceAll(currentChar, diacritics[currentChar]); 32 | } 33 | } 34 | 35 | return str; 36 | } 37 | 38 | function escapeRegExp(string = '') { 39 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 40 | } 41 | 42 | const getPredictiveSuggestions = ({ suggestions, currentValue, wordsToShowAfterHighlight }) => { 43 | const suggestionMap = {}; 44 | if (currentValue) { 45 | const currentValueTrimmed = currentValue.trim(); 46 | const parsedSuggestion = suggestions.reduce((agg, { label, ...rest }) => { 47 | // to handle special strings with pattern 'xyz ${highlightedWord}`; 76 | const suggestionValue = `${currentValueTrimmed}${highlightedWord}`; 77 | // to show unique results only 78 | if (!suggestionMap[suggestionPhrase]) { 79 | suggestionMap[suggestionPhrase] = 1; 80 | return [ 81 | ...agg, 82 | { 83 | ...rest, 84 | label: suggestionPhrase, 85 | value: suggestionValue, 86 | isPredictiveSuggestion: true, 87 | }, 88 | ]; 89 | } 90 | 91 | return agg; 92 | } 93 | 94 | return agg; 95 | }, []); 96 | 97 | return parsedSuggestion; 98 | } 99 | 100 | return []; 101 | }; 102 | 103 | const getSuggestions = ({ 104 | fields, 105 | suggestions, 106 | currentValue, 107 | suggestionProperties = [], 108 | showDistinctSuggestions = false, 109 | enablePredictiveSuggestions = false, 110 | wordsToShowAfterHighlight = 2, 111 | enableSynonyms, 112 | }) => { 113 | /* 114 | fields: DataFields passed on Search Components 115 | suggestions: Raw Suggestions received from ES 116 | currentValue: Search Term 117 | skipWordMatch: Use to skip the word match logic, important for synonym 118 | showDistinctSuggestions: When set to true will only return 1 suggestion per document 119 | */ 120 | 121 | let suggestionsList = []; 122 | let labelsList = []; 123 | let skipWordMatch = false; 124 | 125 | const populateSuggestionsList = (val, parsedSource, source) => { 126 | // check if the suggestion includes the current value 127 | // and not already included in other suggestions 128 | const isWordMatch 129 | = skipWordMatch 130 | || currentValue 131 | .trim() 132 | .split(' ') 133 | .some(term => 134 | replaceDiacritics(val) 135 | .toLowerCase() 136 | .includes(replaceDiacritics(term))); 137 | // promoted results should always include in suggestions even there is no match 138 | if ((isWordMatch && !labelsList.includes(val)) || source._promoted) { 139 | const defaultOption = { 140 | label: val, 141 | value: val, 142 | source, 143 | }; 144 | let additionalKeys = {}; 145 | if (Array.isArray(suggestionProperties) && suggestionProperties.length > 0) { 146 | suggestionProperties.forEach((prop) => { 147 | if (parsedSource.hasOwnProperty(prop)) { 148 | additionalKeys = { 149 | ...additionalKeys, 150 | [prop]: parsedSource[prop], 151 | }; 152 | } 153 | }); 154 | } 155 | 156 | const option = { 157 | ...defaultOption, 158 | ...additionalKeys, 159 | }; 160 | labelsList = [...labelsList, val]; 161 | suggestionsList = [...suggestionsList, option]; 162 | 163 | if (showDistinctSuggestions) { 164 | return true; 165 | } 166 | } 167 | 168 | return false; 169 | }; 170 | 171 | const parseField = (parsedSource, field = '', source = parsedSource) => { 172 | if (typeof parsedSource === 'object') { 173 | const fieldNodes = field.split('.'); 174 | let label = parsedSource[fieldNodes[0]]; 175 | // To handle field names with dots 176 | // For example, if source has a top level field name is `user.name` 177 | // then it would extract the suggestion from parsed source 178 | if (parsedSource[field]) { 179 | const topLabel = parsedSource[field]; 180 | const val = extractSuggestion(topLabel); 181 | if (val && typeof val === 'string') { 182 | return populateSuggestionsList(val, parsedSource, source); 183 | } 184 | } 185 | // if they type of field is array of strings 186 | // then we need to pick first matching value as the label 187 | if (Array.isArray(label)) { 188 | if (label.length > 1) { 189 | label = label.filter(i => 190 | i 191 | && i 192 | .toString() 193 | .toLowerCase() 194 | .includes(currentValue.toString().toLowerCase())); 195 | } 196 | label = label[0]; 197 | } 198 | 199 | if (label) { 200 | if (fieldNodes.length > 1) { 201 | // nested fields of the 'foo.bar.zoo' variety 202 | const children = field.substring(fieldNodes[0].length + 1); 203 | parseField(label, children, source); 204 | } else { 205 | const val = extractSuggestion(label); 206 | if (val) { 207 | return populateSuggestionsList(val, parsedSource, source); 208 | } 209 | } 210 | } 211 | } 212 | return false; 213 | }; 214 | 215 | const traverseSuggestions = () => { 216 | suggestions.forEach((item) => { 217 | fields.forEach((field) => { 218 | parseField(item, field); 219 | }); 220 | }); 221 | }; 222 | 223 | traverseSuggestions(); 224 | 225 | if (suggestionsList.length < suggestions.length && !skipWordMatch && enableSynonyms) { 226 | /* 227 | When we have synonym we set skipWordMatch to false as it may discard 228 | the suggestion if word doesnt match term. 229 | For eg: iphone, ios are synonyms and on searching iphone isWordMatch 230 | in populateSuggestionList may discard ios source which decreases no. 231 | of items in suggestionsList 232 | */ 233 | skipWordMatch = true; 234 | traverseSuggestions(); 235 | } 236 | if (enablePredictiveSuggestions) { 237 | const predictiveSuggestions = getPredictiveSuggestions({ 238 | suggestions: suggestionsList, 239 | currentValue, 240 | wordsToShowAfterHighlight, 241 | }); 242 | suggestionsList = predictiveSuggestions; 243 | } 244 | if (showDistinctSuggestions) { 245 | const idMap = {}; 246 | const filteredSuggestions = []; 247 | suggestionsList.forEach((suggestion) => { 248 | if (suggestion.source && suggestion.source._id) { 249 | if (!idMap[suggestion.source._id]) { 250 | filteredSuggestions.push(suggestion); 251 | idMap[suggestion.source._id] = true; 252 | } 253 | } 254 | }); 255 | 256 | return filteredSuggestions; 257 | } 258 | return suggestionsList; 259 | }; 260 | 261 | export default getSuggestions; 262 | -------------------------------------------------------------------------------- /src/actions/analytics.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SUGGESTIONS_SEARCH_VALUE, 3 | CLEAR_SUGGESTIONS_SEARCH_VALUE, 4 | UPDATE_ANALYTICS_CONFIG, 5 | RECENT_SEARCHES_SUCCESS, 6 | RECENT_SEARCHES_ERROR, 7 | } from '../constants'; 8 | 9 | /** 10 | * Sets the suggestionsSearchValue in analytics 11 | * @param {String} value 12 | */ 13 | export function setSuggestionsSearchValue(value) { 14 | return { 15 | type: SET_SUGGESTIONS_SEARCH_VALUE, 16 | value, 17 | }; 18 | } 19 | 20 | /** 21 | * Clears the suggestionsSearchValue in analytics 22 | * @param {String} value 23 | */ 24 | export function clearSuggestionsSearchValue() { 25 | return { 26 | type: CLEAR_SUGGESTIONS_SEARCH_VALUE, 27 | }; 28 | } 29 | 30 | /** 31 | * Updates the analytics config object 32 | * @param {Object} analyticsConfig 33 | */ 34 | export function updateAnalyticsConfig(analyticsConfig) { 35 | return { 36 | type: UPDATE_ANALYTICS_CONFIG, 37 | analyticsConfig, 38 | }; 39 | } 40 | 41 | export function getRecentSearches(queryOptions = { 42 | size: 5, 43 | minChars: 3, 44 | }) { 45 | return (dispatch, getState) => { 46 | const { 47 | config, 48 | headers, 49 | appbaseRef: { url, protocol, credentials }, 50 | } = getState(); 51 | const { app, mongodb } = config; 52 | const esURL = `${protocol}://${url}`; 53 | const parsedURL = (esURL || '').replace(/\/+$/, ''); 54 | 55 | const requestOptions = { 56 | headers: { 57 | ...headers, 58 | 'Content-Type': 'application/json', 59 | Authorization: `Basic ${btoa(credentials)}`, 60 | }, 61 | }; 62 | let queryString = ''; 63 | const addParam = (key, value) => { 64 | if (queryString) { 65 | queryString += `&${key}=${value}`; 66 | } else { 67 | queryString += `${key}=${value}`; 68 | } 69 | }; 70 | // Add user id in query param if defined 71 | if (config.analyticsConfig && config.analyticsConfig.userId) { 72 | addParam('user_id', config.analyticsConfig.userId); 73 | } 74 | if (queryOptions) { 75 | if (queryOptions.size) { 76 | addParam('size', String(queryOptions.size)); 77 | } 78 | if (queryOptions.from) { 79 | addParam('from', queryOptions.from); 80 | } 81 | if (queryOptions.to) { 82 | addParam('to', queryOptions.to); 83 | } 84 | if (queryOptions.minChars) { 85 | addParam('min_chars', String(queryOptions.minChars)); 86 | } 87 | if (queryOptions.customEvents) { 88 | Object.keys(queryOptions.customEvents).forEach((key) => { 89 | addParam(key, queryOptions.customEvents[key]); 90 | }); 91 | } 92 | } 93 | if (mongodb) { 94 | return dispatch({ 95 | type: RECENT_SEARCHES_SUCCESS, 96 | data: [], 97 | }); 98 | } 99 | return fetch( 100 | `${parsedURL}/_analytics/${app}/recent-searches?${queryString}`, 101 | requestOptions, 102 | ) 103 | .then((res) => { 104 | if (res.status >= 500 || res.status >= 400) { 105 | return dispatch({ 106 | type: RECENT_SEARCHES_ERROR, 107 | error: res, 108 | }); 109 | } 110 | return res 111 | .json() 112 | .then(recentSearches => 113 | dispatch({ 114 | type: RECENT_SEARCHES_SUCCESS, 115 | data: recentSearches, 116 | })) 117 | .catch(e => 118 | dispatch({ 119 | type: RECENT_SEARCHES_ERROR, 120 | error: e, 121 | })); 122 | }) 123 | .catch(e => 124 | dispatch({ 125 | type: RECENT_SEARCHES_ERROR, 126 | error: e, 127 | })); 128 | }; 129 | } 130 | 131 | function recordClick({ 132 | documentId, clickPosition, analyticsInstance, isSuggestionClick, 133 | }) { 134 | if (!documentId) { 135 | console.warn('ReactiveSearch: document id is required to record the click analytics'); 136 | } else { 137 | analyticsInstance.click({ 138 | queryID: analyticsInstance.getQueryID(), 139 | objects: { 140 | [documentId]: clickPosition + 1, 141 | }, 142 | isSuggestionClick, 143 | }); 144 | } 145 | } 146 | 147 | export function recordResultClick(searchPosition, documentId) { 148 | return (dispatch, getState) => { 149 | const { 150 | config, 151 | analytics: { searchId }, 152 | headers, 153 | appbaseRef: { url, protocol, credentials }, 154 | analyticsRef: analyticsInstance, 155 | } = getState(); 156 | const { app } = config; 157 | const esURL = `${protocol}://${url}`; 158 | if (config.analytics && searchId) { 159 | const parsedHeaders = headers; 160 | delete parsedHeaders['X-Search-Query']; 161 | const parsedURL = (esURL || '').replace(/\/+$/, ''); 162 | if (parsedURL.includes('scalr.api.appbase.io')) { 163 | fetch(`${parsedURL}/${app}/_analytics`, { 164 | method: 'POST', 165 | headers: { 166 | ...parsedHeaders, 167 | 'Content-Type': 'application/json', 168 | Authorization: `Basic ${btoa(credentials)}`, 169 | 'X-Search-Id': searchId, 170 | 'X-Search-Click': true, 171 | 'X-Search-ClickPosition': searchPosition + 1, 172 | }, 173 | }); 174 | } else { 175 | recordClick({ 176 | documentId, 177 | clickPosition: searchPosition, 178 | analyticsInstance, 179 | }); 180 | } 181 | } 182 | }; 183 | } 184 | 185 | export function recordSuggestionClick(searchPosition, documentId) { 186 | return (dispatch, getState) => { 187 | const { 188 | config, 189 | analytics: { suggestionsSearchId }, 190 | headers, 191 | appbaseRef: { url, protocol, credentials }, 192 | analyticsRef: analyticsInstance, 193 | } = getState(); 194 | const { app } = config; 195 | const esURL = `${protocol}://${url}`; 196 | if ( 197 | config.analytics 198 | && (config.analyticsConfig === undefined 199 | || config.analyticsConfig.suggestionAnalytics === undefined 200 | || config.analyticsConfig.suggestionAnalytics) 201 | ) { 202 | const parsedHeaders = headers; 203 | delete parsedHeaders['X-Search-Query']; 204 | const parsedURL = (esURL || '').replace(/\/+$/, ''); 205 | if ( 206 | parsedURL.includes('scalr.api.appbase.io') 207 | && searchPosition !== undefined 208 | && suggestionsSearchId 209 | ) { 210 | fetch(`${parsedURL}/${app}/_analytics`, { 211 | method: 'POST', 212 | headers: { 213 | ...parsedHeaders, 214 | 'Content-Type': 'application/json', 215 | Authorization: `Basic ${btoa(credentials)}`, 216 | 'X-Search-Id': suggestionsSearchId, 217 | 'X-Search-Suggestions-Click': true, 218 | 'X-Search-Suggestions-ClickPosition': searchPosition + 1, 219 | }, 220 | }); 221 | } else if (searchPosition !== undefined) { 222 | recordClick({ 223 | documentId, 224 | clickPosition: searchPosition, 225 | analyticsInstance, 226 | isSuggestionClick: true, 227 | }); 228 | } 229 | } 230 | }; 231 | } 232 | 233 | // impressions represents an array of impression objects, for e.g {"index": "test", "id": 1213} 234 | export function recordImpressions(queryId, impressions = []) { 235 | return (dispatch, getState) => { 236 | const { 237 | appbaseRef: { url, protocol }, 238 | analyticsRef: analyticsInstance, 239 | config, 240 | } = getState(); 241 | const esURL = `${protocol}://${url}`; 242 | const parsedURL = esURL.replace(/\/+$/, ''); 243 | if ( 244 | config.analytics 245 | && !parsedURL.includes('scalr.api.appbase.io') 246 | && queryId 247 | && impressions.length 248 | ) { 249 | analyticsInstance.search({ 250 | queryID: analyticsInstance.getQueryID(), 251 | impressions, 252 | }); 253 | } 254 | }; 255 | } 256 | 257 | export function recordAISessionUsefulness(sessionId, otherInfo) { 258 | return (dispatch, getState) => { 259 | const { analyticsRef: analyticsInstance, config } = getState(); 260 | if (!config || !config.analyticsConfig || !config.analyticsConfig.recordAnalytics) { 261 | console.warn('ReactiveSearch: Unable to record usefulness of session. To enable analytics, make sure to include the following prop on your component: reactivesearchAPIConfig={{ recordAnalytics: true }}'); 262 | return; 263 | } 264 | 265 | const userID = config && config.analyticsConfig && config.analyticsConfig.userId; 266 | if (!sessionId) { 267 | console.warn('ReactiveSearch: AI sessionID is required to record the usefulness of session.'); 268 | return; 269 | } 270 | // Save session usefulness 271 | analyticsInstance.saveSessionUsefulness( 272 | sessionId, 273 | { 274 | ...otherInfo, 275 | userID, 276 | }, 277 | (err, res) => { 278 | // eslint-disable-next-line no-console 279 | 280 | res._bodyBlob 281 | .text() 282 | .then((textData) => { 283 | try { 284 | const parsedErrorRes = JSON.parse(textData); 285 | if (parsedErrorRes.error) { 286 | const errorCode = parsedErrorRes.error.code; 287 | const errorMessage = parsedErrorRes.error.message; 288 | 289 | let finalErrorMessage 290 | = 'There was an error recording the usefulness of the session. \n\n'; 291 | if (errorCode) { 292 | finalErrorMessage += errorCode; 293 | } 294 | if (errorMessage) { 295 | finalErrorMessage += `${errorCode ? ': ' : ''}${errorMessage}`; 296 | } 297 | console.error(finalErrorMessage); 298 | } 299 | } catch (error) { 300 | console.error('There was an error recording the usefulness of the session. \n\n'); 301 | } 302 | }) 303 | .catch((error) => { 304 | console.error('Error reading component error text data:', error); 305 | }); 306 | }, 307 | ); 308 | }; 309 | } 310 | -------------------------------------------------------------------------------- /src/actions/utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | setError, 3 | setLoading, 4 | setSuggestionsSearchId, 5 | setSearchId, 6 | setAppliedSettings, 7 | setPromotedResults, 8 | setRawData, 9 | setCustomData, 10 | setTimestamp, 11 | setLastUsedAppbaseQuery, 12 | setAIResponse, 13 | setAIResponseError, 14 | } from './misc'; 15 | 16 | import { updateHits, updateAggs, updateCompositeAggs, saveQueryToHits } from './hits'; 17 | import { getInternalComponentID } from '../utils/transform'; 18 | import { UPDATE_CONFIG } from '../constants'; 19 | import { fetchAIResponse } from './query'; 20 | import { AI_LOCAL_CACHE_KEY, componentTypes } from '../utils/constants'; 21 | import { getObjectFromLocalStorage, setObjectInLocalStorage } from '../utils/helper'; 22 | 23 | export const handleTransformResponse = (res = null, config = {}, component = '') => { 24 | if (config.transformResponse && typeof config.transformResponse === 'function') { 25 | return config.transformResponse(res, component); 26 | } 27 | return new Promise(resolve => resolve(res)); 28 | }; 29 | 30 | // Checks if a component is active or not at a particular time 31 | export const isComponentActive = (getState = () => {}, componentId = '') => { 32 | const { components } = getState(); 33 | if (components.includes(componentId)) { 34 | return true; 35 | } 36 | return false; 37 | }; 38 | 39 | export const getQuerySuggestionsId = (componentId = '') => `${componentId}__suggestions`; 40 | 41 | export const handleError = ( 42 | { orderOfQueries = [], error = null } = {}, 43 | getState = () => {}, 44 | dispatch, 45 | ) => { 46 | const { queryListener } = getState(); 47 | try { 48 | console.error(JSON.stringify(error)); 49 | } catch (e) { 50 | console.error(error); 51 | } 52 | 53 | orderOfQueries.forEach((component) => { 54 | if (isComponentActive(getState, component)) { 55 | // Only update state for active components 56 | if (queryListener[component] && queryListener[component].onError) { 57 | queryListener[component].onError(error); 58 | } 59 | dispatch(setError(component, error)); 60 | dispatch(setLoading(component, false)); 61 | } 62 | }); 63 | }; 64 | 65 | export const handleResponse = ( 66 | { 67 | res, 68 | orderOfQueries = [], 69 | appendToHits = false, 70 | appendToAggs = false, 71 | isSuggestionsQuery = false, 72 | query, 73 | queryId, 74 | } = {}, 75 | getState = () => {}, 76 | dispatch, 77 | ) => { 78 | const { 79 | config, internalValues, lastUsedAppbaseQuery, analyticsRef, 80 | } = getState(); 81 | 82 | const searchId = res._headers ? res._headers.get('X-Search-Id') : null; 83 | if (searchId) { 84 | if (isSuggestionsQuery) { 85 | // set suggestions search id for internal request of search components 86 | dispatch(setSuggestionsSearchId(searchId)); 87 | } else { 88 | // if search id was updated set it in store 89 | dispatch(setSearchId(searchId)); 90 | if (analyticsRef) { 91 | analyticsRef.queryID = searchId; 92 | } 93 | } 94 | } 95 | 96 | // handle promoted results 97 | orderOfQueries.forEach((component) => { 98 | // Only update state for active components 99 | if (isComponentActive(getState, component)) { 100 | // Avoid settings stale results 101 | if ( 102 | lastUsedAppbaseQuery[component] 103 | && lastUsedAppbaseQuery[component].queryId 104 | && queryId 105 | && lastUsedAppbaseQuery[component].queryId !== queryId 106 | ) { 107 | return; 108 | } 109 | // Update applied settings 110 | if (res.settings) { 111 | dispatch(setAppliedSettings(res.settings, component)); 112 | } 113 | handleTransformResponse(res[component], config, component) 114 | .then((response) => { 115 | if (response) { 116 | const { timestamp, props } = getState(); 117 | if ( 118 | timestamp[component] === undefined 119 | || timestamp[component] < res._timestamp 120 | || (response.AISessionId 121 | && props[component].enableAI 122 | && props[component].componentType === componentTypes.searchBox) 123 | ) { 124 | const promotedResults = response.promoted; 125 | if (promotedResults) { 126 | const parsedPromotedResults = promotedResults.map(promoted => ({ 127 | ...promoted.doc, 128 | _position: promoted.position, 129 | })); 130 | dispatch(setPromotedResults(parsedPromotedResults, component)); 131 | } else { 132 | dispatch(setPromotedResults([], component)); 133 | } 134 | // set raw response in rawData 135 | dispatch(setRawData(component, response)); 136 | 137 | if (response.AIAnswer) { 138 | if (response.AIAnswer.error) { 139 | dispatch(setAIResponseError(component, { 140 | message: response.AIAnswer.error, 141 | })); 142 | dispatch(setLoading(component, false)); 143 | return; 144 | } 145 | const input = response.AIAnswer; 146 | // store direct answer returned from API call 147 | const finalResponse = { 148 | answer: { 149 | documentIds: input.documentIds, 150 | model: input.model, 151 | text: input.choices[0].message.content, 152 | }, 153 | }; 154 | const finalResponseObj = { 155 | response: finalResponse, 156 | meta: response.hits, 157 | isTyping: false, 158 | }; 159 | if (response.AISessionId) { 160 | finalResponseObj.sessionId = response.AISessionId; 161 | } 162 | dispatch(setAIResponse(component, finalResponseObj)); 163 | } else if (response.AISessionId) { 164 | const localCache = (getObjectFromLocalStorage(AI_LOCAL_CACHE_KEY) 165 | || {})[props.componentId]; 166 | if ( 167 | localCache 168 | && localCache.sessionId 169 | && localCache.sessionId === response.AISessionId 170 | ) { 171 | // hydrate the store from cache 172 | dispatch(setAIResponse(component, localCache)); 173 | } else { 174 | // delete localCache 175 | setObjectInLocalStorage('AISessions', { 176 | [component]: {}, 177 | }); 178 | 179 | // rely on trigger from within the component 180 | // to fetch initial result for AIAnswer component 181 | if ( 182 | props[component].componentType === componentTypes.AIAnswer 183 | ) { 184 | dispatch(setAIResponse(component, { 185 | sessionId: response.AISessionId, 186 | meta: { 187 | hits: response.hits || {}, 188 | }, 189 | })); 190 | } else { 191 | // fetch initial AIResponse 192 | dispatch(fetchAIResponse( 193 | response.AISessionId, 194 | component, 195 | '', 196 | { 197 | hits: response.hits || {}, 198 | }, 199 | props[component].componentType 200 | === componentTypes.searchBox 201 | || props[component].componentType 202 | === componentTypes.AIAnswer, // make extra GET call to fetch meta info 203 | )); 204 | } 205 | } 206 | } 207 | 208 | // Update custom data 209 | dispatch(setCustomData(response.customData, component)); 210 | if ( 211 | response.hits 212 | && !( 213 | (response.AIAnswer || response.AISessionId) 214 | && props[component].componentType === componentTypes.searchBox 215 | ) 216 | ) { 217 | dispatch(setTimestamp(component, res._timestamp)); 218 | // store last used query for REACTIVE_LIST only 219 | 220 | if ( 221 | props[component].componentType 222 | === componentTypes.reactiveList 223 | && query.find(queryItem => queryItem.id === component).execute 224 | ) { 225 | dispatch(setLastUsedAppbaseQuery({ [component]: query })); 226 | } 227 | 228 | dispatch(updateHits( 229 | component, 230 | response.hits, 231 | response.took, 232 | response.hits && response.hits.hidden, 233 | appendToHits, 234 | )); 235 | // get query value 236 | const internalComponentID = getInternalComponentID(component); 237 | // Store the last query value associated with `hits` 238 | if (internalValues[internalComponentID]) { 239 | dispatch(saveQueryToHits( 240 | component, 241 | internalValues[internalComponentID].value, 242 | )); 243 | } 244 | } 245 | 246 | if (response.aggregations) { 247 | dispatch(updateAggs(component, response.aggregations, appendToAggs)); 248 | dispatch(updateCompositeAggs( 249 | component, 250 | response.aggregations, 251 | appendToAggs, 252 | )); 253 | } 254 | } 255 | dispatch(setLoading(component, false)); 256 | } 257 | }) 258 | .catch((err) => { 259 | handleError( 260 | { 261 | orderOfQueries, 262 | error: err, 263 | }, 264 | getState, 265 | dispatch, 266 | ); 267 | }); 268 | } 269 | // } 270 | }); 271 | }; 272 | 273 | export const isPropertyDefined = property => property !== undefined && property !== null; 274 | 275 | export const getSuggestionQuery = (getState = () => {}, componentId) => { 276 | const { internalValues } = getState(); 277 | const internalValue = internalValues[componentId]; 278 | const value = (internalValue && internalValue.value) || ''; 279 | return [ 280 | { 281 | id: getQuerySuggestionsId(componentId), 282 | dataField: ['key', 'key.autosuggest'], 283 | size: 5, 284 | value, 285 | defaultQuery: { 286 | query: { 287 | bool: { 288 | minimum_should_match: 1, 289 | should: [ 290 | { 291 | function_score: { 292 | field_value_factor: { 293 | field: 'count', 294 | modifier: 'sqrt', 295 | missing: 1, 296 | }, 297 | }, 298 | }, 299 | { 300 | multi_match: { 301 | fields: ['key^9', 'key.autosuggest^1', 'key.keyword^10'], 302 | fuzziness: 0, 303 | operator: 'or', 304 | query: value, 305 | type: 'best_fields', 306 | }, 307 | }, 308 | { 309 | multi_match: { 310 | fields: ['key^9', 'key.autosuggest^1', 'key.keyword^10'], 311 | operator: 'or', 312 | query: value, 313 | type: 'phrase', 314 | }, 315 | }, 316 | { 317 | multi_match: { 318 | fields: ['key^9'], 319 | operator: 'or', 320 | query: value, 321 | type: 'phrase_prefix', 322 | }, 323 | }, 324 | ], 325 | }, 326 | }, 327 | }, 328 | }, 329 | ]; 330 | }; 331 | 332 | export function executeQueryListener(listener, oldQuery, newQuery) { 333 | if (listener && listener.onQueryChange) { 334 | listener.onQueryChange(oldQuery, newQuery); 335 | } 336 | } 337 | 338 | export function updateStoreConfig(payload) { 339 | return (dispatch) => { 340 | dispatch({ 341 | type: UPDATE_CONFIG, 342 | config: payload, 343 | }); 344 | }; 345 | } 346 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/utils/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { queryTypes } from './constants'; 3 | import { handleTransformResponse, isPropertyDefined } from '../actions/utils'; 4 | import { 5 | componentToTypeMap, 6 | extractPropsFromState, 7 | getDependentQueries, 8 | getRSQuery, 9 | } from './transform'; 10 | 11 | import valueReducer from '../reducers/valueReducer'; 12 | import { buildQuery } from './helper'; 13 | 14 | function getValue(state, id, defaultValue) { 15 | if (state && state[id]) { 16 | try { 17 | // parsing for next.js - since it uses extra set of quotes to wrap params 18 | const parsedValue = JSON.parse(state[id]); 19 | 20 | return { 21 | ...(typeof parsedValue === 'object' && parsedValue.value 22 | ? { 23 | value: parsedValue.value, 24 | ...(parsedValue.category ? { category: parsedValue.category } : {}), 25 | } 26 | : { value: parsedValue }), 27 | reference: 'URL', 28 | }; 29 | } catch (error) { 30 | // using react-dom-server for ssr 31 | return { 32 | value: state[id], 33 | reference: 'URL', 34 | }; 35 | } 36 | } 37 | return { 38 | value: defaultValue, 39 | reference: 'DEFAULT', 40 | }; 41 | } 42 | 43 | // parse query string 44 | // ref: https://stackoverflow.com/a/13896633/10822996 45 | function parseQuery(str) { 46 | if (str instanceof Object) { 47 | return str; 48 | } 49 | if (typeof str !== 'string' || str.length === 0) return {}; 50 | let s; 51 | if (str.split('/?')[1]) { 52 | s = str.split('/?')[1].split('&'); 53 | } 54 | if (str.split('?')[1]) { 55 | s = str.split('?')[1].split('&'); 56 | } 57 | 58 | if (!s) return {}; 59 | const sLength = s.length; 60 | let bit; 61 | const query = {}; 62 | let first; 63 | let second; 64 | 65 | for (let i = 0; i < sLength; i += 1) { 66 | bit = s[i].split('='); 67 | first = decodeURIComponent(bit[0]); 68 | // eslint-disable-next-line no-continue 69 | if (first.length === 0) continue; 70 | second = decodeURIComponent(bit[1]); 71 | if (typeof query[first] === 'undefined') query[first] = second; 72 | else if (query[first] instanceof Array) query[first].push(second); 73 | else query[first] = [query[first], second]; 74 | } 75 | return query; 76 | } 77 | 78 | const getServerResults = () => { 79 | let storeReference = null; 80 | 81 | return (App, queryString = '', ssrRenderFunc) => { 82 | try { 83 | // parse the query String to respect url params in SSR 84 | const parsedQueryString = parseQuery(queryString); 85 | if (!storeReference) { 86 | let newSelectedValues = {}; 87 | // callback function to collect SearchBase context 88 | const contextCollector = (params) => { 89 | if (params.ctx) { 90 | // store collected 91 | storeReference = params.ctx; 92 | 93 | // collect selected values from the URL query string 94 | Object.keys(parsedQueryString).forEach((componentId) => { 95 | const { value, reference } = getValue( 96 | parsedQueryString, 97 | componentId, 98 | null, 99 | ); 100 | if (value) { 101 | newSelectedValues = valueReducer(newSelectedValues, { 102 | type: 'PATCH_VALUE', 103 | component: componentId, 104 | payload: { 105 | value, 106 | reference, 107 | }, 108 | }); 109 | } 110 | }); 111 | } 112 | 113 | return { 114 | // send back to ReactiveBase to hydrate the store with values from queryParams(if present) 115 | selectedValues: newSelectedValues, 116 | }; 117 | }; 118 | 119 | // render the app server-side to collect context and build initial state 120 | // for hydration on client side 121 | // in case of React, ssrRenderFunc === renderToString || renderToStaticMarkup 122 | // in case of Vue, ssrRenderFunc === renderToString (import { renderToString } from 'vue/server-renderer') 123 | const output = ssrRenderFunc(App({ contextCollector })); 124 | let promiseFunc; 125 | if (!output.then) { 126 | promiseFunc = Promise.resolve(promiseFunc); 127 | } else { 128 | promiseFunc = output; 129 | } 130 | return promiseFunc 131 | .then(() => { 132 | if (storeReference) { 133 | const extractedState = storeReference.getState(); 134 | const { 135 | components, 136 | config, 137 | appbaseRef, 138 | queryOptions, 139 | internalValues, 140 | props, 141 | queryList, 142 | dependencyTree, 143 | } = extractedState; 144 | 145 | let { queryLog } = extractedState; 146 | 147 | let finalQuery = []; 148 | let appbaseQuery = {}; // Use object to prevent duplicate query added by react prop 149 | let orderOfQueries = []; 150 | let hits = {}; 151 | let aggregations = {}; 152 | let state = { ...extractedState }; 153 | 154 | // Generate finalQuery for search 155 | components 156 | .filter(t => !t.endsWith('__internal')) 157 | .forEach((componentId) => { 158 | // eslint-disable-next-line 159 | let { queryObj, options } = buildQuery( 160 | componentId, 161 | dependencyTree, 162 | queryList, 163 | queryOptions, 164 | ); 165 | if (!queryObj && !options) { 166 | return; 167 | } 168 | 169 | const query = getRSQuery( 170 | componentId, 171 | extractPropsFromState( 172 | state, 173 | componentId, 174 | queryOptions && queryOptions[componentId] 175 | ? { from: queryOptions[componentId].from } 176 | : null, 177 | ), 178 | ); 179 | 180 | // check if query or options are valid - non-empty 181 | if (query && !!Object.keys(query).length) { 182 | const currentQuery = query; 183 | const dependentQueries = getDependentQueries( 184 | state, 185 | componentId, 186 | orderOfQueries, 187 | ); 188 | 189 | let queryToLog = { 190 | ...{ [componentId]: currentQuery }, 191 | ...Object.keys(dependentQueries).reduce( 192 | (acc, q) => ({ 193 | ...acc, 194 | [q]: { 195 | ...dependentQueries[q], 196 | execute: false, 197 | ...(dependentQueries[q].type 198 | === queryTypes.suggestion 199 | ? { type: 'search' } 200 | : {}), 201 | }, 202 | }), 203 | {}, 204 | ), 205 | }; 206 | if ( 207 | [queryTypes.range, queryTypes.term].includes(componentToTypeMap[ 208 | props[componentId].componentType 209 | ]) 210 | ) { 211 | // Avoid logging `value` for term type of components 212 | // eslint-disable-next-line 213 | const { value, ...rest } = currentQuery; 214 | 215 | queryToLog = { 216 | ...{ [componentId]: rest }, 217 | ...Object.keys(dependentQueries).reduce( 218 | (acc, q) => ({ 219 | ...acc, 220 | [q]: { 221 | ...dependentQueries[q], 222 | execute: false, 223 | ...(dependentQueries[q].type 224 | === queryTypes.suggestion 225 | ? { type: 'search' } 226 | : {}), 227 | }, 228 | }), 229 | {}, 230 | ), 231 | }; 232 | } 233 | // if (!compareQueries(queryToLog, currentQuery, false)) { 234 | orderOfQueries = [...orderOfQueries, componentId]; 235 | queryLog = { 236 | ...queryLog, 237 | [componentId]: queryToLog, 238 | }; 239 | // } 240 | 241 | if (query) { 242 | // Apply dependent queries 243 | const dependentQueriesToAppend = getDependentQueries( 244 | state, 245 | componentId, 246 | orderOfQueries, 247 | ); 248 | appbaseQuery = { 249 | ...appbaseQuery, 250 | ...{ [componentId]: query }, 251 | }; 252 | Object.keys(dependentQueriesToAppend).forEach((cId) => { 253 | if (appbaseQuery[cId]) { 254 | appbaseQuery[cId + Math.random()] 255 | = dependentQueriesToAppend[cId]; 256 | } else { 257 | appbaseQuery[cId] 258 | = dependentQueriesToAppend[cId]; 259 | } 260 | }); 261 | } 262 | } 263 | }); 264 | 265 | const handleRSResponse = (res) => { 266 | const promotedResults = {}; 267 | const rawData = {}; 268 | const customData = {}; 269 | const allPromises = orderOfQueries.map(component => 270 | new Promise((responseResolve, responseReject) => { 271 | handleTransformResponse( 272 | res[component], 273 | config, 274 | component, 275 | ) 276 | .then((response) => { 277 | if (response) { 278 | if (response.promoted) { 279 | promotedResults[component] 280 | = response.promoted.map(promoted => ({ 281 | ...promoted.doc, 282 | _position: 283 | promoted.position, 284 | })); 285 | } 286 | rawData[component] = response; 287 | // Update custom data 288 | if (response.customData) { 289 | customData[component] 290 | = response.customData; 291 | } 292 | 293 | if (response.aggregations) { 294 | aggregations = { 295 | ...aggregations, 296 | [component]: response.aggregations, 297 | }; 298 | } 299 | const hitsObj = response.hits || {}; 300 | hits = { 301 | ...hits, 302 | [component]: { 303 | hits: hitsObj.hits, 304 | total: 305 | typeof hitsObj.total 306 | === 'object' 307 | ? hitsObj.total.value 308 | : hitsObj.total, 309 | time: response.took, 310 | }, 311 | }; 312 | responseResolve(); 313 | } 314 | }) 315 | .catch((err) => { 316 | responseReject(err); 317 | }); 318 | })); 319 | 320 | return Promise.all(allPromises).then(() => { 321 | state = { 322 | queryList, 323 | queryOptions, 324 | selectedValues: newSelectedValues, 325 | internalValues, 326 | queryLog, 327 | hits, 328 | aggregations, 329 | promotedResults, 330 | customData, 331 | rawData, 332 | dependencyTree, 333 | }; 334 | return Promise.resolve(JSON.parse(JSON.stringify(state))); 335 | }); 336 | }; 337 | 338 | if (Object.keys(appbaseQuery).length) { 339 | finalQuery = Object.values(appbaseQuery); 340 | // Call RS API 341 | const rsAPISettings = {}; 342 | if (config.analyticsConfig) { 343 | rsAPISettings.recordAnalytics = isPropertyDefined(config.analyticsConfig.recordAnalytics) 344 | ? config.analyticsConfig.recordAnalytics 345 | : undefined; 346 | rsAPISettings.userId = isPropertyDefined(config.analyticsConfig.userId) 347 | ? config.analyticsConfig.userId 348 | : undefined; 349 | rsAPISettings.enableQueryRules = isPropertyDefined(config.analyticsConfig.enableQueryRules) 350 | ? config.analyticsConfig.enableQueryRules 351 | : undefined; 352 | rsAPISettings.customEvents = isPropertyDefined(config.analyticsConfig.customEvents) 353 | ? config.analyticsConfig.customEvents 354 | : undefined; 355 | } 356 | 357 | return appbaseRef 358 | .reactiveSearchv3(finalQuery, rsAPISettings) 359 | .then(res => handleRSResponse(res)) 360 | .catch(err => Promise.reject(err)); 361 | } 362 | throw new Error('Could not compute server-side initial state of the app!'); 363 | } else { 364 | return null; 365 | } 366 | }) 367 | .catch(Promise.reject); 368 | } 369 | return null; 370 | } catch (error) { 371 | return Promise.reject(error); 372 | } 373 | }; 374 | }; 375 | // eslint-disable-next-line 376 | export { getServerResults }; 377 | -------------------------------------------------------------------------------- /src/utils/diacritics.js: -------------------------------------------------------------------------------- 1 | const diacritics = { 2 | '«': '"', 3 | '²': '2', 4 | '³': '3', 5 | '¹': '1', 6 | '»': '"', 7 | À: 'A', 8 | Á: 'A', 9 | Â: 'A', 10 | Ã: 'A', 11 | Ä: 'A', 12 | Å: 'A', 13 | Æ: 'AE', 14 | Ç: 'C', 15 | È: 'E', 16 | É: 'E', 17 | Ê: 'E', 18 | Ë: 'E', 19 | Ì: 'I', 20 | Í: 'I', 21 | Î: 'I', 22 | Ï: 'I', 23 | Ð: 'D', 24 | Ñ: 'N', 25 | Ò: 'O', 26 | Ó: 'O', 27 | Ô: 'O', 28 | Õ: 'O', 29 | Ö: 'O', 30 | Ø: 'O', 31 | Ù: 'U', 32 | Ú: 'U', 33 | Û: 'U', 34 | Ü: 'U', 35 | Ý: 'Y', 36 | Þ: 'TH', 37 | ß: 'ss', 38 | à: 'a', 39 | á: 'a', 40 | â: 'a', 41 | ã: 'a', 42 | ä: 'a', 43 | å: 'a', 44 | æ: 'ae', 45 | ç: 'c', 46 | è: 'e', 47 | é: 'e', 48 | ê: 'e', 49 | ë: 'e', 50 | ì: 'i', 51 | í: 'i', 52 | î: 'i', 53 | ï: 'i', 54 | ð: 'd', 55 | ñ: 'n', 56 | ò: 'o', 57 | ó: 'o', 58 | ô: 'o', 59 | õ: 'o', 60 | ö: 'o', 61 | ø: 'o', 62 | ù: 'u', 63 | ú: 'u', 64 | û: 'u', 65 | ü: 'u', 66 | ý: 'y', 67 | þ: 'th', 68 | ÿ: 'y', 69 | Ā: 'A', 70 | ā: 'a', 71 | Ă: 'A', 72 | ă: 'a', 73 | Ą: 'A', 74 | ą: 'a', 75 | Ć: 'C', 76 | ć: 'c', 77 | Ĉ: 'C', 78 | ĉ: 'c', 79 | Ċ: 'C', 80 | ċ: 'c', 81 | Č: 'C', 82 | č: 'c', 83 | Ď: 'D', 84 | ď: 'd', 85 | Đ: 'D', 86 | đ: 'd', 87 | Ē: 'E', 88 | ē: 'e', 89 | Ĕ: 'E', 90 | ĕ: 'e', 91 | Ė: 'E', 92 | ė: 'e', 93 | Ę: 'E', 94 | ę: 'e', 95 | Ě: 'E', 96 | ě: 'e', 97 | Ĝ: 'G', 98 | ĝ: 'g', 99 | Ğ: 'G', 100 | ğ: 'g', 101 | Ġ: 'G', 102 | ġ: 'g', 103 | Ģ: 'G', 104 | ģ: 'g', 105 | Ĥ: 'H', 106 | ĥ: 'h', 107 | Ħ: 'H', 108 | ħ: 'h', 109 | Ĩ: 'I', 110 | ĩ: 'i', 111 | Ī: 'I', 112 | ī: 'i', 113 | Ĭ: 'I', 114 | ĭ: 'i', 115 | Į: 'I', 116 | į: 'i', 117 | İ: 'I', 118 | ı: 'i', 119 | IJ: 'IJ', 120 | ij: 'ij', 121 | Ĵ: 'J', 122 | ĵ: 'j', 123 | Ķ: 'K', 124 | ķ: 'k', 125 | ĸ: 'q', 126 | Ĺ: 'L', 127 | ĺ: 'l', 128 | Ļ: 'L', 129 | ļ: 'l', 130 | Ľ: 'L', 131 | ľ: 'l', 132 | Ŀ: 'L', 133 | ŀ: 'l', 134 | Ł: 'L', 135 | ł: 'l', 136 | Ń: 'N', 137 | ń: 'n', 138 | Ņ: 'N', 139 | ņ: 'n', 140 | Ň: 'N', 141 | ň: 'n', 142 | ʼn: 'n', 143 | Ŋ: 'N', 144 | ŋ: 'n', 145 | Ō: 'O', 146 | ō: 'o', 147 | Ŏ: 'O', 148 | ŏ: 'o', 149 | Ő: 'O', 150 | ő: 'o', 151 | Œ: 'OE', 152 | œ: 'oe', 153 | Ŕ: 'R', 154 | ŕ: 'r', 155 | Ŗ: 'R', 156 | ŗ: 'r', 157 | Ř: 'R', 158 | ř: 'r', 159 | Ś: 'S', 160 | ś: 's', 161 | Ŝ: 'S', 162 | ŝ: 's', 163 | Ş: 'S', 164 | ş: 's', 165 | Š: 'S', 166 | š: 's', 167 | Ţ: 'T', 168 | ţ: 't', 169 | Ť: 'T', 170 | ť: 't', 171 | Ŧ: 'T', 172 | ŧ: 't', 173 | Ũ: 'U', 174 | ũ: 'u', 175 | Ū: 'U', 176 | ū: 'u', 177 | Ŭ: 'U', 178 | ŭ: 'u', 179 | Ů: 'U', 180 | ů: 'u', 181 | Ű: 'U', 182 | ű: 'u', 183 | Ų: 'U', 184 | ų: 'u', 185 | Ŵ: 'W', 186 | ŵ: 'w', 187 | Ŷ: 'Y', 188 | ŷ: 'y', 189 | Ÿ: 'Y', 190 | Ź: 'Z', 191 | ź: 'z', 192 | Ż: 'Z', 193 | ż: 'z', 194 | Ž: 'Z', 195 | ž: 'z', 196 | ſ: 's', 197 | ƀ: 'b', 198 | Ɓ: 'B', 199 | Ƃ: 'B', 200 | ƃ: 'b', 201 | Ɔ: 'O', 202 | Ƈ: 'C', 203 | ƈ: 'c', 204 | Ɖ: 'D', 205 | Ɗ: 'D', 206 | Ƌ: 'D', 207 | ƌ: 'd', 208 | Ǝ: 'E', 209 | Ə: 'A', 210 | Ɛ: 'E', 211 | Ƒ: 'F', 212 | ƒ: 'f', 213 | Ɠ: 'G', 214 | ƕ: 'hv', 215 | Ɩ: 'I', 216 | Ɨ: 'I', 217 | Ƙ: 'K', 218 | ƙ: 'k', 219 | ƚ: 'l', 220 | Ɯ: 'M', 221 | Ɲ: 'N', 222 | ƞ: 'n', 223 | Ɵ: 'O', 224 | Ơ: 'O', 225 | ơ: 'o', 226 | Ƥ: 'P', 227 | ƥ: 'p', 228 | ƫ: 't', 229 | Ƭ: 'T', 230 | ƭ: 't', 231 | Ʈ: 'T', 232 | Ư: 'U', 233 | ư: 'u', 234 | Ʋ: 'V', 235 | Ƴ: 'Y', 236 | ƴ: 'y', 237 | Ƶ: 'Z', 238 | ƶ: 'z', 239 | ƿ: 'w', 240 | DŽ: 'DZ', 241 | Dž: 'Dz', 242 | dž: 'dz', 243 | LJ: 'LJ', 244 | Lj: 'Lj', 245 | lj: 'lj', 246 | NJ: 'NJ', 247 | Nj: 'Nj', 248 | nj: 'nj', 249 | Ǎ: 'A', 250 | ǎ: 'a', 251 | Ǐ: 'I', 252 | ǐ: 'i', 253 | Ǒ: 'O', 254 | ǒ: 'o', 255 | Ǔ: 'U', 256 | ǔ: 'u', 257 | Ǖ: 'U', 258 | ǖ: 'u', 259 | Ǘ: 'U', 260 | ǘ: 'u', 261 | Ǚ: 'U', 262 | ǚ: 'u', 263 | Ǜ: 'U', 264 | ǜ: 'u', 265 | ǝ: 'e', 266 | Ǟ: 'A', 267 | ǟ: 'a', 268 | Ǡ: 'A', 269 | ǡ: 'a', 270 | Ǣ: 'AE', 271 | ǣ: 'ae', 272 | Ǥ: 'G', 273 | ǥ: 'G', 274 | Ǧ: 'G', 275 | ǧ: 'G', 276 | Ǩ: 'K', 277 | ǩ: 'k', 278 | Ǫ: 'O', 279 | ǫ: 'o', 280 | Ǭ: 'O', 281 | ǭ: 'o', 282 | ǰ: 'j', 283 | DZ: 'DZ', 284 | Dz: 'Dz', 285 | dz: 'dz', 286 | Ǵ: 'G', 287 | ǵ: 'g', 288 | Ƕ: 'HV', 289 | Ƿ: 'W', 290 | Ǹ: 'N', 291 | ǹ: 'n', 292 | Ǻ: 'A', 293 | ǻ: 'a', 294 | Ǽ: 'AE', 295 | ǽ: 'ae', 296 | Ǿ: 'O', 297 | ǿ: 'o', 298 | Ȁ: 'A', 299 | ȁ: 'a', 300 | Ȃ: 'A', 301 | ȃ: 'a', 302 | Ȅ: 'E', 303 | ȅ: 'e', 304 | Ȇ: 'E', 305 | ȇ: 'e', 306 | Ȉ: 'I', 307 | ȉ: 'i', 308 | Ȋ: 'I', 309 | ȋ: 'i', 310 | Ȍ: 'O', 311 | ȍ: 'o', 312 | Ȏ: 'O', 313 | ȏ: 'o', 314 | Ȑ: 'R', 315 | ȑ: 'r', 316 | Ȓ: 'R', 317 | ȓ: 'r', 318 | Ȕ: 'U', 319 | ȕ: 'u', 320 | Ȗ: 'U', 321 | ȗ: 'u', 322 | Ș: 'S', 323 | ș: 's', 324 | Ț: 'T', 325 | ț: 't', 326 | Ȝ: 'Z', 327 | ȝ: 'z', 328 | Ȟ: 'H', 329 | ȟ: 'h', 330 | Ƞ: 'N', 331 | ȡ: 'd', 332 | Ȣ: 'OU', 333 | ȣ: 'ou', 334 | Ȥ: 'Z', 335 | ȥ: 'z', 336 | Ȧ: 'A', 337 | ȧ: 'a', 338 | Ȩ: 'E', 339 | ȩ: 'e', 340 | Ȫ: 'O', 341 | ȫ: 'o', 342 | Ȭ: 'O', 343 | ȭ: 'o', 344 | Ȯ: 'O', 345 | ȯ: 'o', 346 | Ȱ: 'O', 347 | ȱ: 'o', 348 | Ȳ: 'Y', 349 | ȳ: 'y', 350 | ȴ: 'l', 351 | ȵ: 'n', 352 | ȶ: 't', 353 | ȷ: 'j', 354 | ȸ: 'db', 355 | ȹ: 'qp', 356 | Ⱥ: 'A', 357 | Ȼ: 'C', 358 | ȼ: 'c', 359 | Ƚ: 'L', 360 | Ⱦ: 'T', 361 | ȿ: 's', 362 | ɀ: 'z', 363 | Ƀ: 'B', 364 | Ʉ: 'U', 365 | Ʌ: 'V', 366 | Ɇ: 'E', 367 | ɇ: 'e', 368 | Ɉ: 'J', 369 | ɉ: 'j', 370 | Ɋ: 'Q', 371 | ɋ: 'q', 372 | Ɍ: 'R', 373 | ɍ: 'r', 374 | Ɏ: 'Y', 375 | ɏ: 'y', 376 | ɐ: 'a', 377 | ɓ: 'b', 378 | ɔ: 'o', 379 | ɕ: 'c', 380 | ɖ: 'd', 381 | ɗ: 'd', 382 | ɘ: 'e', 383 | ə: 'a', 384 | ɚ: 'a', 385 | ɛ: 'e', 386 | ɜ: 'e', 387 | ɝ: 'e', 388 | ɞ: 'e', 389 | ɟ: 'j', 390 | ɠ: 'g', 391 | ɡ: 'g', 392 | ɢ: 'G', 393 | ɥ: 'h', 394 | ɦ: 'h', 395 | ɨ: 'i', 396 | ɪ: 'I', 397 | ɫ: 'l', 398 | ɬ: 'l', 399 | ɭ: 'l', 400 | ɯ: 'm', 401 | ɰ: 'm', 402 | ɱ: 'm', 403 | ɲ: 'n', 404 | ɳ: 'n', 405 | ɴ: 'N', 406 | ɵ: 'o', 407 | ɶ: 'OE', 408 | ɼ: 'r', 409 | ɽ: 'r', 410 | ɾ: 'r', 411 | ɿ: 'r', 412 | ʀ: 'R', 413 | ʁ: 'R', 414 | ʂ: 's', 415 | ʄ: 'j', 416 | ʇ: 't', 417 | ʈ: 't', 418 | ʉ: 'u', 419 | ʋ: 'v', 420 | ʌ: 'v', 421 | ʍ: 'w', 422 | ʎ: 'y', 423 | ʏ: 'Y', 424 | ʐ: 'z', 425 | ʑ: 'z', 426 | ʗ: 'C', 427 | ʙ: 'B', 428 | ʚ: 'e', 429 | ʛ: 'G', 430 | ʜ: 'H', 431 | ʝ: 'j', 432 | ʞ: 'k', 433 | ʟ: 'L', 434 | ʠ: 'q', 435 | ʣ: 'dz', 436 | ʥ: 'dz', 437 | ʦ: 'ts', 438 | ʨ: 'tc', 439 | ʪ: 'ls', 440 | ʫ: 'lz', 441 | ʮ: 'h', 442 | ʯ: 'h', 443 | ᴀ: 'A', 444 | ᴁ: 'AE', 445 | ᴂ: 'ae', 446 | ᴃ: 'B', 447 | ᴄ: 'C', 448 | ᴅ: 'D', 449 | ᴆ: 'D', 450 | ᴇ: 'E', 451 | ᴈ: 'e', 452 | ᴉ: 'i', 453 | ᴊ: 'J', 454 | ᴋ: 'K', 455 | ᴌ: 'L', 456 | ᴍ: 'M', 457 | ᴎ: 'N', 458 | ᴏ: 'O', 459 | ᴐ: 'O', 460 | ᴔ: 'oe', 461 | ᴕ: 'OU', 462 | ᴖ: 'o', 463 | ᴗ: 'o', 464 | ᴘ: 'P', 465 | ᴙ: 'R', 466 | ᴚ: 'R', 467 | ᴛ: 'T', 468 | ᴜ: 'U', 469 | ᴠ: 'V', 470 | ᴡ: 'W', 471 | ᴢ: 'Z', 472 | ᵢ: 'i', 473 | ᵣ: 'r', 474 | ᵤ: 'u', 475 | ᵥ: 'v', 476 | ᵫ: 'ue', 477 | ᵬ: 'b', 478 | ᵭ: 'd', 479 | ᵮ: 'f', 480 | ᵯ: 'm', 481 | ᵰ: 'n', 482 | ᵱ: 'p', 483 | ᵲ: 'r', 484 | ᵳ: 'r', 485 | ᵴ: 's', 486 | ᵵ: 't', 487 | ᵶ: 'z', 488 | ᵷ: 'g', 489 | ᵹ: 'g', 490 | ᵺ: 'th', 491 | ᵻ: 'I', 492 | ᵼ: 'i', 493 | ᵽ: 'p', 494 | ᵾ: 'U', 495 | ᶀ: 'b', 496 | ᶁ: 'd', 497 | ᶂ: 'f', 498 | ᶃ: 'g', 499 | ᶄ: 'k', 500 | ᶅ: 'l', 501 | ᶆ: 'm', 502 | ᶇ: 'n', 503 | ᶈ: 'p', 504 | ᶉ: 'r', 505 | ᶊ: 's', 506 | ᶌ: 'v', 507 | ᶍ: 'x', 508 | ᶎ: 'z', 509 | ᶏ: 'a', 510 | ᶑ: 'd', 511 | ᶒ: 'e', 512 | ᶓ: 'e', 513 | ᶔ: 'e', 514 | ᶕ: 'a', 515 | ᶖ: 'i', 516 | ᶗ: 'o', 517 | ᶙ: 'u', 518 | Ḁ: 'A', 519 | ḁ: 'a', 520 | Ḃ: 'B', 521 | ḃ: 'b', 522 | Ḅ: 'B', 523 | ḅ: 'b', 524 | Ḇ: 'B', 525 | ḇ: 'b', 526 | Ḉ: 'C', 527 | ḉ: 'c', 528 | Ḋ: 'D', 529 | ḋ: 'd', 530 | Ḍ: 'D', 531 | ḍ: 'd', 532 | Ḏ: 'D', 533 | ḏ: 'd', 534 | Ḑ: 'D', 535 | ḑ: 'd', 536 | Ḓ: 'D', 537 | ḓ: 'd', 538 | Ḕ: 'E', 539 | ḕ: 'e', 540 | Ḗ: 'E', 541 | ḗ: 'e', 542 | Ḙ: 'E', 543 | ḙ: 'e', 544 | Ḛ: 'E', 545 | ḛ: 'e', 546 | Ḝ: 'E', 547 | ḝ: 'e', 548 | Ḟ: 'F', 549 | ḟ: 'f', 550 | Ḡ: 'G', 551 | ḡ: 'g', 552 | Ḣ: 'H', 553 | ḣ: 'h', 554 | Ḥ: 'H', 555 | ḥ: 'h', 556 | Ḧ: 'H', 557 | ḧ: 'h', 558 | Ḩ: 'H', 559 | ḩ: 'h', 560 | Ḫ: 'H', 561 | ḫ: 'h', 562 | Ḭ: 'I', 563 | ḭ: 'i', 564 | Ḯ: 'I', 565 | ḯ: 'i', 566 | Ḱ: 'K', 567 | ḱ: 'k', 568 | Ḳ: 'K', 569 | ḳ: 'k', 570 | Ḵ: 'K', 571 | ḵ: 'k', 572 | Ḷ: 'L', 573 | ḷ: 'l', 574 | Ḹ: 'L', 575 | ḹ: 'l', 576 | Ḻ: 'L', 577 | ḻ: 'l', 578 | Ḽ: 'L', 579 | ḽ: 'l', 580 | Ḿ: 'M', 581 | ḿ: 'm', 582 | Ṁ: 'M', 583 | ṁ: 'm', 584 | Ṃ: 'M', 585 | ṃ: 'm', 586 | Ṅ: 'N', 587 | ṅ: 'n', 588 | Ṇ: 'N', 589 | ṇ: 'n', 590 | Ṉ: 'N', 591 | ṉ: 'n', 592 | Ṋ: 'N', 593 | ṋ: 'n', 594 | Ṍ: 'O', 595 | ṍ: 'o', 596 | Ṏ: 'O', 597 | ṏ: 'o', 598 | Ṑ: 'O', 599 | ṑ: 'o', 600 | Ṓ: 'O', 601 | ṓ: 'o', 602 | Ṕ: 'P', 603 | ṕ: 'p', 604 | Ṗ: 'P', 605 | ṗ: 'p', 606 | Ṙ: 'R', 607 | ṙ: 'r', 608 | Ṛ: 'R', 609 | ṛ: 'r', 610 | Ṝ: 'R', 611 | ṝ: 'r', 612 | Ṟ: 'R', 613 | ṟ: 'r', 614 | Ṡ: 'S', 615 | ṡ: 's', 616 | Ṣ: 'S', 617 | ṣ: 's', 618 | Ṥ: 'S', 619 | ṥ: 's', 620 | Ṧ: 'S', 621 | ṧ: 's', 622 | Ṩ: 'S', 623 | ṩ: 's', 624 | Ṫ: 'T', 625 | ṫ: 't', 626 | Ṭ: 'T', 627 | ṭ: 't', 628 | Ṯ: 'T', 629 | ṯ: 't', 630 | Ṱ: 'T', 631 | ṱ: 't', 632 | Ṳ: 'U', 633 | ṳ: 'u', 634 | Ṵ: 'U', 635 | ṵ: 'u', 636 | Ṷ: 'U', 637 | ṷ: 'u', 638 | Ṹ: 'U', 639 | ṹ: 'u', 640 | Ṻ: 'U', 641 | ṻ: 'u', 642 | Ṽ: 'V', 643 | ṽ: 'v', 644 | Ṿ: 'V', 645 | ṿ: 'v', 646 | Ẁ: 'W', 647 | ẁ: 'w', 648 | Ẃ: 'W', 649 | ẃ: 'w', 650 | Ẅ: 'W', 651 | ẅ: 'w', 652 | Ẇ: 'W', 653 | ẇ: 'w', 654 | Ẉ: 'W', 655 | ẉ: 'w', 656 | Ẋ: 'X', 657 | ẋ: 'x', 658 | Ẍ: 'X', 659 | ẍ: 'x', 660 | Ẏ: 'Y', 661 | ẏ: 'y', 662 | Ẑ: 'Z', 663 | ẑ: 'z', 664 | Ẓ: 'Z', 665 | ẓ: 'z', 666 | Ẕ: 'Z', 667 | ẕ: 'z', 668 | ẖ: 'h', 669 | ẗ: 't', 670 | ẘ: 'w', 671 | ẙ: 'y', 672 | ẚ: 'a', 673 | ẛ: 'f', 674 | ẜ: 's', 675 | ẝ: 's', 676 | ẞ: 'SS', 677 | Ạ: 'A', 678 | ạ: 'a', 679 | Ả: 'A', 680 | ả: 'a', 681 | Ấ: 'A', 682 | ấ: 'a', 683 | Ầ: 'A', 684 | ầ: 'a', 685 | Ẩ: 'A', 686 | ẩ: 'a', 687 | Ẫ: 'A', 688 | ẫ: 'a', 689 | Ậ: 'A', 690 | ậ: 'a', 691 | Ắ: 'A', 692 | ắ: 'a', 693 | Ằ: 'A', 694 | ằ: 'a', 695 | Ẳ: 'A', 696 | ẳ: 'a', 697 | Ẵ: 'A', 698 | ẵ: 'a', 699 | Ặ: 'A', 700 | ặ: 'a', 701 | Ẹ: 'E', 702 | ẹ: 'e', 703 | Ẻ: 'E', 704 | ẻ: 'e', 705 | Ẽ: 'E', 706 | ẽ: 'e', 707 | Ế: 'E', 708 | ế: 'e', 709 | Ề: 'E', 710 | ề: 'e', 711 | Ể: 'E', 712 | ể: 'e', 713 | Ễ: 'E', 714 | ễ: 'e', 715 | Ệ: 'E', 716 | ệ: 'e', 717 | Ỉ: 'I', 718 | ỉ: 'i', 719 | Ị: 'I', 720 | ị: 'i', 721 | Ọ: 'O', 722 | ọ: 'o', 723 | Ỏ: 'O', 724 | ỏ: 'o', 725 | Ố: 'O', 726 | ố: 'o', 727 | Ồ: 'O', 728 | ồ: 'o', 729 | Ổ: 'O', 730 | ổ: 'o', 731 | Ỗ: 'O', 732 | ỗ: 'o', 733 | Ộ: 'O', 734 | ộ: 'o', 735 | Ớ: 'O', 736 | ớ: 'o', 737 | Ờ: 'O', 738 | ờ: 'o', 739 | Ở: 'O', 740 | ở: 'o', 741 | Ỡ: 'O', 742 | ỡ: 'o', 743 | Ợ: 'O', 744 | ợ: 'o', 745 | Ụ: 'U', 746 | ụ: 'u', 747 | Ủ: 'U', 748 | ủ: 'u', 749 | Ứ: 'U', 750 | ứ: 'u', 751 | Ừ: 'U', 752 | ừ: 'u', 753 | Ử: 'U', 754 | ử: 'u', 755 | Ữ: 'U', 756 | ữ: 'u', 757 | Ự: 'U', 758 | ự: 'u', 759 | Ỳ: 'Y', 760 | ỳ: 'y', 761 | Ỵ: 'Y', 762 | ỵ: 'y', 763 | Ỷ: 'Y', 764 | ỷ: 'y', 765 | Ỹ: 'Y', 766 | ỹ: 'y', 767 | Ỻ: 'LL', 768 | ỻ: 'll', 769 | Ỽ: 'V', 770 | Ỿ: 'Y', 771 | ỿ: 'y', 772 | '‐': '-', 773 | '‑': '-', 774 | '‒': '-', 775 | '–': '-', 776 | '—': '-', 777 | '‘': '"', 778 | '’': '"', 779 | '‚': '"', 780 | '‛': '"', 781 | '“': '"', 782 | '”': '"', 783 | '„': '"', 784 | '′': '"', 785 | '″': '"', 786 | '‵': '"', 787 | '‶': '"', 788 | '‸': '^', 789 | '‹': '"', 790 | '›': '"', 791 | '‼': '!!', 792 | '⁄': '/', 793 | '⁅': '[', 794 | '⁆': ']', 795 | '⁇': '??', 796 | '⁈': '?!', 797 | '⁉': '!?', 798 | '⁎': '*', 799 | '⁏': ';', 800 | '⁒': '%', 801 | '⁓': '~', 802 | '⁰': '0', 803 | ⁱ: 'i', 804 | '⁴': '4', 805 | '⁵': '5', 806 | '⁶': '6', 807 | '⁷': '7', 808 | '⁸': '8', 809 | '⁹': '9', 810 | '⁺': '+', 811 | '⁻': '-', 812 | '⁼': '=', 813 | '⁽': '(', 814 | '⁾': ')', 815 | ⁿ: 'n', 816 | '₀': '0', 817 | '₁': '1', 818 | '₂': '2', 819 | '₃': '3', 820 | '₄': '4', 821 | '₅': '5', 822 | '₆': '6', 823 | '₇': '7', 824 | '₈': '8', 825 | '₉': '9', 826 | '₊': '+', 827 | '₋': '-', 828 | '₌': '=', 829 | '₍': '(', 830 | '₎': ')', 831 | ₐ: 'a', 832 | ₑ: 'e', 833 | ₒ: 'o', 834 | ₓ: 'x', 835 | ₔ: 'a', 836 | ↄ: 'c', 837 | '①': '1', 838 | '②': '2', 839 | '③': '3', 840 | '④': '4', 841 | '⑤': '5', 842 | '⑥': '6', 843 | '⑦': '7', 844 | '⑧': '8', 845 | '⑨': '9', 846 | '⑩': '10', 847 | '⑪': '11', 848 | '⑫': '12', 849 | '⑬': '13', 850 | '⑭': '14', 851 | '⑮': '15', 852 | '⑯': '16', 853 | '⑰': '17', 854 | '⑱': '18', 855 | '⑲': '19', 856 | '⑳': '20', 857 | '⑴': '(1)', 858 | '⑵': '(2)', 859 | '⑶': '(3)', 860 | '⑷': '(4)', 861 | '⑸': '(5)', 862 | '⑹': '(6)', 863 | '⑺': '(7)', 864 | '⑻': '(8)', 865 | '⑼': '(9)', 866 | '⑽': '(10)', 867 | '⑾': '(11)', 868 | '⑿': '(12)', 869 | '⒀': '(13)', 870 | '⒁': '(14)', 871 | '⒂': '(15)', 872 | '⒃': '(16)', 873 | '⒄': '(17)', 874 | '⒅': '(18)', 875 | '⒆': '(19)', 876 | '⒇': '(20)', 877 | '⒈': '1.', 878 | '⒉': '2.', 879 | '⒊': '3.', 880 | '⒋': '4.', 881 | '⒌': '5.', 882 | '⒍': '6.', 883 | '⒎': '7.', 884 | '⒏': '8.', 885 | '⒐': '9.', 886 | '⒑': '10.', 887 | '⒒': '11.', 888 | '⒓': '12.', 889 | '⒔': '13.', 890 | '⒕': '14.', 891 | '⒖': '15.', 892 | '⒗': '16.', 893 | '⒘': '17.', 894 | '⒙': '18.', 895 | '⒚': '19.', 896 | '⒛': '20.', 897 | '⒜': '(a)', 898 | '⒝': '(b)', 899 | '⒞': '(c)', 900 | '⒟': '(d)', 901 | '⒠': '(e)', 902 | '⒡': '(f)', 903 | '⒢': '(g)', 904 | '⒣': '(h)', 905 | '⒤': '(i)', 906 | '⒥': '(j)', 907 | '⒦': '(k)', 908 | '⒧': '(l)', 909 | '⒨': '(m)', 910 | '⒩': '(n)', 911 | '⒪': '(o)', 912 | '⒫': '(p)', 913 | '⒬': '(q)', 914 | '⒭': '(r)', 915 | '⒮': '(s)', 916 | '⒯': '(t)', 917 | '⒰': '(u)', 918 | '⒱': '(v)', 919 | '⒲': '(w)', 920 | '⒳': '(x)', 921 | '⒴': '(y)', 922 | '⒵': '(z)', 923 | 'Ⓐ': 'A', 924 | 'Ⓑ': 'B', 925 | 'Ⓒ': 'C', 926 | 'Ⓓ': 'D', 927 | 'Ⓔ': 'E', 928 | 'Ⓕ': 'F', 929 | 'Ⓖ': 'G', 930 | 'Ⓗ': 'H', 931 | 'Ⓘ': 'I', 932 | 'Ⓙ': 'J', 933 | 'Ⓚ': 'K', 934 | 'Ⓛ': 'L', 935 | 'Ⓜ': 'M', 936 | 'Ⓝ': 'N', 937 | 'Ⓞ': 'O', 938 | 'Ⓟ': 'P', 939 | 'Ⓠ': 'Q', 940 | 'Ⓡ': 'R', 941 | 'Ⓢ': 'S', 942 | 'Ⓣ': 'T', 943 | 'Ⓤ': 'U', 944 | 'Ⓥ': 'V', 945 | 'Ⓦ': 'W', 946 | 'Ⓧ': 'X', 947 | 'Ⓨ': 'Y', 948 | 'Ⓩ': 'Z', 949 | 'ⓐ': 'a', 950 | 'ⓑ': 'b', 951 | 'ⓒ': 'c', 952 | 'ⓓ': 'd', 953 | 'ⓔ': 'e', 954 | 'ⓕ': 'f', 955 | 'ⓖ': 'g', 956 | 'ⓗ': 'h', 957 | 'ⓘ': 'i', 958 | 'ⓙ': 'j', 959 | 'ⓚ': 'k', 960 | 'ⓛ': 'l', 961 | 'ⓜ': 'm', 962 | 'ⓝ': 'n', 963 | 'ⓞ': 'o', 964 | 'ⓟ': 'p', 965 | 'ⓠ': 'q', 966 | 'ⓡ': 'r', 967 | 'ⓢ': 's', 968 | 'ⓣ': 't', 969 | 'ⓤ': 'u', 970 | 'ⓥ': 'v', 971 | 'ⓦ': 'w', 972 | 'ⓧ': 'x', 973 | 'ⓨ': 'y', 974 | 'ⓩ': 'z', 975 | '⓪': '0', 976 | '⓫': '11', 977 | '⓬': '12', 978 | '⓭': '13', 979 | '⓮': '14', 980 | '⓯': '15', 981 | '⓰': '16', 982 | '⓱': '17', 983 | '⓲': '18', 984 | '⓳': '19', 985 | '⓴': '20', 986 | '⓵': '1', 987 | '⓶': '2', 988 | '⓷': '3', 989 | '⓸': '4', 990 | '⓹': '5', 991 | '⓺': '6', 992 | '⓻': '7', 993 | '⓼': '8', 994 | '⓽': '9', 995 | '⓾': '10', 996 | '⓿': '0', 997 | '❛': '"', 998 | '❜': '"', 999 | '❝': '"', 1000 | '❞': '"', 1001 | '❨': '(', 1002 | '❩': ')', 1003 | '❪': '(', 1004 | '❫': ')', 1005 | '❬': '<', 1006 | '❭': '>', 1007 | '❮': '"', 1008 | '❯': '"', 1009 | '❰': '<', 1010 | '❱': '>', 1011 | '❲': '[', 1012 | '❳': ']', 1013 | '❴': '{', 1014 | '❵': '}', 1015 | '❶': '1', 1016 | '❷': '2', 1017 | '❸': '3', 1018 | '❹': '4', 1019 | '❺': '5', 1020 | '❻': '6', 1021 | '❼': '7', 1022 | '❽': '8', 1023 | '❾': '9', 1024 | '❿': '10', 1025 | '➀': '1', 1026 | '➁': '2', 1027 | '➂': '3', 1028 | '➃': '4', 1029 | '➄': '5', 1030 | '➅': '6', 1031 | '➆': '7', 1032 | '➇': '8', 1033 | '➈': '9', 1034 | '➉': '10', 1035 | '➊': '1', 1036 | '➋': '2', 1037 | '➌': '3', 1038 | '➍': '4', 1039 | '➎': '5', 1040 | '➏': '6', 1041 | '➐': '7', 1042 | '➑': '8', 1043 | '➒': '9', 1044 | '➓': '10', 1045 | Ⱡ: 'L', 1046 | ⱡ: 'l', 1047 | Ɫ: 'L', 1048 | Ᵽ: 'P', 1049 | Ɽ: 'R', 1050 | ⱥ: 'a', 1051 | ⱦ: 't', 1052 | Ⱨ: 'H', 1053 | ⱨ: 'h', 1054 | Ⱪ: 'K', 1055 | ⱪ: 'k', 1056 | Ⱬ: 'Z', 1057 | ⱬ: 'z', 1058 | Ɱ: 'M', 1059 | Ɐ: 'a', 1060 | ⱱ: 'v', 1061 | Ⱳ: 'W', 1062 | ⱳ: 'w', 1063 | ⱴ: 'v', 1064 | Ⱶ: 'H', 1065 | ⱶ: 'h', 1066 | ⱸ: 'e', 1067 | ⱺ: 'o', 1068 | ⱻ: 'E', 1069 | ⱼ: 'j', 1070 | '⸨': '((', 1071 | '⸩': '))', 1072 | Ꜩ: 'TZ', 1073 | ꜩ: 'tz', 1074 | ꜰ: 'F', 1075 | ꜱ: 'S', 1076 | Ꜳ: 'AA', 1077 | ꜳ: 'aa', 1078 | Ꜵ: 'AO', 1079 | ꜵ: 'ao', 1080 | Ꜷ: 'AU', 1081 | ꜷ: 'au', 1082 | Ꜹ: 'AV', 1083 | ꜹ: 'av', 1084 | Ꜻ: 'AV', 1085 | ꜻ: 'av', 1086 | Ꜽ: 'AY', 1087 | ꜽ: 'ay', 1088 | Ꜿ: 'c', 1089 | ꜿ: 'c', 1090 | Ꝁ: 'K', 1091 | ꝁ: 'k', 1092 | Ꝃ: 'K', 1093 | ꝃ: 'k', 1094 | Ꝅ: 'K', 1095 | ꝅ: 'k', 1096 | Ꝇ: 'L', 1097 | ꝇ: 'l', 1098 | Ꝉ: 'L', 1099 | ꝉ: 'l', 1100 | Ꝋ: 'O', 1101 | ꝋ: 'o', 1102 | Ꝍ: 'O', 1103 | ꝍ: 'o', 1104 | Ꝏ: 'OO', 1105 | ꝏ: 'oo', 1106 | Ꝑ: 'P', 1107 | ꝑ: 'p', 1108 | Ꝓ: 'P', 1109 | ꝓ: 'p', 1110 | Ꝕ: 'P', 1111 | ꝕ: 'p', 1112 | Ꝗ: 'Q', 1113 | ꝗ: 'q', 1114 | Ꝙ: 'Q', 1115 | ꝙ: 'q', 1116 | Ꝛ: 'R', 1117 | ꝛ: 'r', 1118 | Ꝟ: 'V', 1119 | ꝟ: 'v', 1120 | Ꝡ: 'VY', 1121 | ꝡ: 'vy', 1122 | Ꝣ: 'Z', 1123 | ꝣ: 'z', 1124 | Ꝧ: 'TH', 1125 | ꝧ: 'th', 1126 | Ꝩ: 'V', 1127 | Ꝺ: 'D', 1128 | ꝺ: 'd', 1129 | Ꝼ: 'F', 1130 | ꝼ: 'f', 1131 | Ᵹ: 'G', 1132 | Ꝿ: 'G', 1133 | ꝿ: 'g', 1134 | Ꞁ: 'L', 1135 | ꞁ: 'l', 1136 | Ꞃ: 'R', 1137 | ꞃ: 'r', 1138 | Ꞅ: 's', 1139 | ꞅ: 'S', 1140 | Ꞇ: 'T', 1141 | ꟻ: 'F', 1142 | ꟼ: 'p', 1143 | ꟽ: 'M', 1144 | ꟾ: 'I', 1145 | ꟿ: 'M', 1146 | ff: 'ff', 1147 | fi: 'fi', 1148 | fl: 'fl', 1149 | ffi: 'ffi', 1150 | ffl: 'ffl', 1151 | st: 'st', 1152 | '!': '!', 1153 | '"': '"', 1154 | '#': '#', 1155 | '$': '$', 1156 | '%': '%', 1157 | '&': '&', 1158 | ''': '"', 1159 | '(': '(', 1160 | ')': ')', 1161 | '*': '*', 1162 | '+': '+', 1163 | ',': ',', 1164 | '-': '-', 1165 | '.': '.', 1166 | '/': '/', 1167 | '0': '0', 1168 | '1': '1', 1169 | '2': '2', 1170 | '3': '3', 1171 | '4': '4', 1172 | '5': '5', 1173 | '6': '6', 1174 | '7': '7', 1175 | '8': '8', 1176 | '9': '9', 1177 | ':': ':', 1178 | ';': ';', 1179 | '<': '<', 1180 | '=': '=', 1181 | '>': '>', 1182 | '?': '?', 1183 | '@': '@', 1184 | A: 'A', 1185 | B: 'B', 1186 | C: 'C', 1187 | D: 'D', 1188 | E: 'E', 1189 | F: 'F', 1190 | G: 'G', 1191 | H: 'H', 1192 | I: 'I', 1193 | J: 'J', 1194 | K: 'K', 1195 | L: 'L', 1196 | M: 'M', 1197 | N: 'N', 1198 | O: 'O', 1199 | P: 'P', 1200 | Q: 'Q', 1201 | R: 'R', 1202 | S: 'S', 1203 | T: 'T', 1204 | U: 'U', 1205 | V: 'V', 1206 | W: 'W', 1207 | X: 'X', 1208 | Y: 'Y', 1209 | Z: 'Z', 1210 | '[': '[', 1211 | '\': '\\', 1212 | ']': ']', 1213 | '^': '^', 1214 | '_': '_', 1215 | a: 'a', 1216 | b: 'b', 1217 | c: 'c', 1218 | d: 'd', 1219 | e: 'e', 1220 | f: 'f', 1221 | g: 'g', 1222 | h: 'h', 1223 | i: 'i', 1224 | j: 'j', 1225 | k: 'k', 1226 | l: 'l', 1227 | m: 'm', 1228 | n: 'n', 1229 | o: 'o', 1230 | p: 'p', 1231 | q: 'q', 1232 | r: 'r', 1233 | s: 's', 1234 | t: 't', 1235 | u: 'u', 1236 | v: 'v', 1237 | w: 'w', 1238 | x: 'x', 1239 | y: 'y', 1240 | z: 'z', 1241 | '{': '{', 1242 | '}': '}', 1243 | '~': '~', 1244 | }; 1245 | 1246 | export default diacritics; 1247 | -------------------------------------------------------------------------------- /src/utils/transform.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import { componentTypes, queryTypes } from './constants'; 3 | import dateFormats from './dateFormats'; 4 | import { formatDate, isValidDateRangeQueryFormat } from './helper'; 5 | 6 | export const componentToTypeMap = { 7 | // search components 8 | [componentTypes.reactiveList]: queryTypes.search, 9 | [componentTypes.dataSearch]: queryTypes.search, 10 | [componentTypes.categorySearch]: queryTypes.search, 11 | [componentTypes.searchBox]: queryTypes.suggestion, 12 | [componentTypes.AIAnswer]: queryTypes.search, 13 | // term components 14 | [componentTypes.singleList]: queryTypes.term, 15 | [componentTypes.multiList]: queryTypes.term, 16 | [componentTypes.singleDataList]: queryTypes.term, 17 | [componentTypes.singleDropdownList]: queryTypes.term, 18 | [componentTypes.multiDataList]: queryTypes.term, 19 | [componentTypes.multiDropdownList]: queryTypes.term, 20 | [componentTypes.tagCloud]: queryTypes.term, 21 | [componentTypes.toggleButton]: queryTypes.term, 22 | [componentTypes.reactiveChart]: queryTypes.term, 23 | [componentTypes.treeList]: queryTypes.term, 24 | // basic components 25 | [componentTypes.numberBox]: queryTypes.term, 26 | 27 | // range components 28 | [componentTypes.datePicker]: queryTypes.range, 29 | [componentTypes.dateRange]: queryTypes.range, 30 | [componentTypes.dynamicRangeSlider]: queryTypes.range, 31 | [componentTypes.singleDropdownRange]: queryTypes.range, 32 | [componentTypes.multiDropdownRange]: queryTypes.range, 33 | [componentTypes.singleRange]: queryTypes.range, 34 | [componentTypes.multiRange]: queryTypes.range, 35 | [componentTypes.rangeSlider]: queryTypes.range, 36 | [componentTypes.ratingsFilter]: queryTypes.range, 37 | [componentTypes.rangeInput]: queryTypes.range, 38 | 39 | // map components 40 | [componentTypes.geoDistanceDropdown]: queryTypes.geo, 41 | [componentTypes.geoDistanceSlider]: queryTypes.geo, 42 | [componentTypes.reactiveMap]: queryTypes.geo, 43 | }; 44 | 45 | const multiRangeComponents = [componentTypes.multiRange, componentTypes.multiDropdownRange]; 46 | const dateRangeComponents = [componentTypes.dateRange, componentTypes.datePicker]; 47 | const searchComponents = [ 48 | componentTypes.categorySearch, 49 | componentTypes.dataSearch, 50 | componentTypes.searchBox, 51 | ]; 52 | const listComponentsWithPagination = [ 53 | componentTypes.singleList, 54 | componentTypes.multiList, 55 | componentTypes.singleDropdownList, 56 | componentTypes.multiDropdownList, 57 | ]; 58 | 59 | export const getNormalizedField = (field) => { 60 | if (field && !Array.isArray(field)) { 61 | return [field]; 62 | } 63 | return field; 64 | }; 65 | 66 | export const isInternalComponent = (componentID = '') => componentID.endsWith('__internal'); 67 | 68 | export const getInternalComponentID = (componentID = '') => `${componentID}__internal`; 69 | 70 | export const getHistogramComponentID = (componentID = '') => `${componentID}__histogram__internal`; 71 | 72 | export const isDRSRangeComponent = (componentID = '') => componentID.endsWith('__range__internal'); 73 | 74 | export const isSearchComponent = (componentType = '') => searchComponents.includes(componentType); 75 | 76 | export const isComponentUsesLabelAsValue = (componentType = '') => 77 | componentType === componentTypes.multiDataList 78 | || componentType === componentTypes.singleDataList 79 | || componentType === componentTypes.tabDataList; 80 | 81 | export const hasPaginationSupport = (componentType = '') => 82 | listComponentsWithPagination.includes(componentType); 83 | 84 | 85 | export const getRSQuery = (componentId, props, execute = true) => { 86 | if (props && componentId) { 87 | const queryType = props.type ? props.type : componentToTypeMap[props.componentType]; 88 | // dataField is a required field for components other than search 89 | // TODO: Revisit this logic based on the Appbase version 90 | // dataField is no longer a required field in RS API 91 | if ( 92 | props.componentType !== componentTypes.AIAnswer 93 | && !isSearchComponent(props.componentType) 94 | && !props.dataField && !props.vectorDataField 95 | ) { 96 | return null; 97 | } 98 | let endpoint; 99 | let compoundClause = props.compoundClause; 100 | if (props.endpoint instanceof Object) { 101 | endpoint = props.endpoint; 102 | } 103 | let featuredSuggestionsProps = { 104 | enableFeaturedSuggestions: props.enableFeaturedSuggestions, 105 | featuredSuggestionsConfig: props.featuredSuggestionsConfig, 106 | }; 107 | let faqSuggestionsProps = { 108 | enableFAQSuggestions: props.enableFAQSuggestions, 109 | FAQSuggestionsConfig: props.FAQSuggestionsConfig, 110 | }; 111 | 112 | if (props.enableFAQSuggestions && !props.searchboxId) { 113 | faqSuggestionsProps = {}; 114 | console.error('Reactivesearch Error: You should also pass a searchboxId when passing enableFAQSuggestions as true.\nRefer to Searchbox component documentation specific to frontend frameworks.\n\nReact(https://docs.reactivesearch.io/docs/reactivesearch/react/search/searchbox/)\n\nVue(https://docs.reactivesearch.io/docs/reactivesearch/vue/search/SearchBox/).'); 115 | } 116 | if (props.enableFeaturedSuggestions && !props.searchboxId) { 117 | featuredSuggestionsProps = {}; 118 | console.error('Reactivesearch Error: You should also pass a searchboxId when passing enableFeaturedSuggestions.\nRefer to Searchbox component documentation specific to frontend frameworks.\n\nReact(https://docs.reactivesearch.io/docs/reactivesearch/react/search/searchbox/)\n\nVue(https://docs.reactivesearch.io/docs/reactivesearch/vue/search/SearchBox/).'); 119 | } 120 | if (compoundClause && !['filter', 'must'].includes(compoundClause)) { 121 | console.error("Reactivesearch Error: Invalid prop supplied - compoundClause. Prop can be one of ['filter', 'must']"); 122 | compoundClause = undefined; 123 | } 124 | 125 | 126 | return { 127 | id: componentId, 128 | type: queryType || queryTypes.search, 129 | dataField: getNormalizedField(props.dataField), 130 | execute, 131 | react: props.react, 132 | highlight: props.highlight, 133 | highlightField: getNormalizedField(props.highlightField), 134 | fuzziness: props.fuzziness, 135 | searchOperators: props.searchOperators, 136 | includeFields: props.includeFields, 137 | excludeFields: props.excludeFields, 138 | size: props.size, 139 | aggregationSize: props.aggregationSize, 140 | from: props.from || undefined, // Need to maintain for RL 141 | queryFormat: props.queryFormat, 142 | sortBy: props.sortBy, 143 | fieldWeights: getNormalizedField(props.fieldWeights), 144 | includeNullValues: props.includeNullValues, 145 | aggregationField: props.aggregationField || undefined, 146 | categoryField: props.categoryField || undefined, 147 | missingLabel: props.missingLabel || undefined, 148 | showMissing: props.showMissing, 149 | nestedField: props.nestedField || undefined, 150 | interval: props.interval, 151 | highlightConfig: props.customHighlight || props.highlightConfig, 152 | customQuery: props.customQuery, 153 | defaultQuery: props.defaultQuery, 154 | value: props.value, 155 | categoryValue: props.categoryValue || undefined, 156 | after: props.after || undefined, 157 | aggregations: props.aggregations || undefined, 158 | enableSynonyms: props.enableSynonyms, 159 | selectAllLabel: props.selectAllLabel, 160 | pagination: props.pagination, 161 | queryString: props.queryString, 162 | distinctField: props.distinctField, 163 | distinctFieldConfig: props.distinctFieldConfig, 164 | index: props.index, 165 | compoundClause, 166 | ...(queryType === queryTypes.suggestion 167 | ? { 168 | enablePopularSuggestions: props.enablePopularSuggestions, 169 | enableEndpointSuggestions: props.enableEndpointSuggestions, 170 | enableRecentSuggestions: props.enableRecentSuggestions, 171 | popularSuggestionsConfig: props.popularSuggestionsConfig, 172 | recentSuggestionsConfig: props.recentSuggestionsConfig, 173 | applyStopwords: props.applyStopwords, 174 | customStopwords: props.customStopwords, 175 | enablePredictiveSuggestions: props.enablePredictiveSuggestions, 176 | indexSuggestionsConfig: props.indexSuggestionsConfig, 177 | enableDocumentSuggestions: props.enableDocumentSuggestions, 178 | showDistinctSuggestions: props.showDistinctSuggestions, 179 | documentSuggestionsConfig: props.enableDocumentSuggestions 180 | ? props.documentSuggestionsConfig : undefined, 181 | ...featuredSuggestionsProps, 182 | ...faqSuggestionsProps, 183 | enableIndexSuggestions: props.enableIndexSuggestions, 184 | ...(props.searchboxId ? { searchboxId: props.searchboxId } : {}), 185 | } 186 | : {}), 187 | calendarInterval: props.calendarInterval, 188 | endpoint, 189 | range: props.range, 190 | ...(queryType !== queryTypes.suggestion && props.enableAI && execute 191 | ? { 192 | enableAI: true, 193 | ...(props.AIConfig ? { AIConfig: props.AIConfig } : {}), 194 | execute: true, 195 | } 196 | : {}), 197 | ...(queryType !== queryTypes.suggestion 198 | ? { 199 | vectorDataField: props.vectorDataField || undefined, 200 | imageValue: props.imageValue || undefined, 201 | candidates: props.candidates || props.size || undefined, 202 | } 203 | : {}), 204 | }; 205 | } 206 | return null; 207 | }; 208 | 209 | export const getValidInterval = (interval, range = {}) => { 210 | const min = Math.ceil((range.end - range.start) / 100) || 1; 211 | if (!interval) { 212 | return min; 213 | } else if (interval < min) { 214 | return min; 215 | } 216 | return interval; 217 | }; 218 | 219 | 220 | export const extractPropsFromState = (store, component, customOptions) => { 221 | const componentProps = store.props[component]; 222 | if (!componentProps) { 223 | return null; 224 | } 225 | const queryType = componentProps.type 226 | ? componentProps.type 227 | : componentToTypeMap[componentProps.componentType]; 228 | 229 | const calcValues = store.selectedValues[component]; 230 | let value = calcValues !== undefined && calcValues !== null ? calcValues.value : undefined; 231 | let queryFormat = componentProps.queryFormat; 232 | // calendarInterval only supported when using date types 233 | let calendarInterval; 234 | const { interval } = componentProps; 235 | let type = queryType; 236 | let dataField = componentProps.dataField; 237 | let aggregations = componentProps.aggregations; 238 | let pagination; // pagination for `term` type of queries 239 | let from = componentProps.from; // offset for RL 240 | let range; // applicable for range components supporting histogram 241 | 242 | // For term queries i.e list component `dataField` will be treated as aggregationField 243 | if (queryType === queryTypes.term) { 244 | // Only apply pagination prop for the components which supports it otherwise it can break the UI 245 | if (componentProps.showLoadMore && hasPaginationSupport(componentProps.componentType)) { 246 | pagination = true; 247 | } 248 | // Extract values from components that are type of objects 249 | // This code handles the controlled behavior in list components for e.g ToggleButton 250 | if (value != null && typeof value === 'object' && value.value) { 251 | value = value.value; 252 | } else if (Array.isArray(value)) { 253 | const parsedValue = []; 254 | value.forEach((val) => { 255 | if (val != null && typeof val === 'object' && val.value) { 256 | parsedValue.push(val.value); 257 | } else { 258 | parsedValue.push(val); 259 | } 260 | }); 261 | value = parsedValue; 262 | } 263 | } 264 | if (queryType === queryTypes.range) { 265 | if (Array.isArray(value)) { 266 | if (multiRangeComponents.includes(componentProps.componentType)) { 267 | value = value.map(({ start, end }) => ({ 268 | start, 269 | end, 270 | })); 271 | } else { 272 | value = { 273 | start: value[0], 274 | end: value[1], 275 | }; 276 | } 277 | } else if (componentProps.showHistogram) { 278 | const internalComponentID = getInternalComponentID(component); 279 | let internalComponentValue = store.internalValues[internalComponentID]; 280 | if (!internalComponentValue) { 281 | // Handle dynamic range slider 282 | const histogramComponentID = getHistogramComponentID(component); 283 | internalComponentValue = store.internalValues[histogramComponentID]; 284 | } 285 | if (internalComponentValue && Array.isArray(internalComponentValue.value)) { 286 | value = { 287 | start: internalComponentValue.value[0], 288 | end: internalComponentValue.value[1], 289 | }; 290 | } 291 | } 292 | 293 | if (isDRSRangeComponent(component)) { 294 | aggregations = ['min', 'max']; 295 | } else if (componentProps.showHistogram) { 296 | aggregations = ['histogram']; 297 | } 298 | 299 | // handle number box, number box query changes based on the `queryFormat` value 300 | if ( 301 | componentProps.componentType === componentTypes.dynamicRangeSlider 302 | || componentProps.componentType === componentTypes.rangeSlider 303 | ) { 304 | calendarInterval = Object.keys(dateFormats).includes(queryFormat) 305 | ? componentProps.calendarInterval 306 | : undefined; 307 | 308 | // Set value 309 | if (value) { 310 | if (isValidDateRangeQueryFormat(componentProps.queryFormat)) { 311 | // check if date types are dealt with 312 | value = { 313 | start: formatDate(dayjs(new Date(value.start)), componentProps), 314 | end: formatDate(dayjs(new Date(value.end)), componentProps), 315 | }; 316 | } else { 317 | value = { 318 | start: parseFloat(value.start), 319 | end: parseFloat(value.end), 320 | }; 321 | } 322 | } 323 | 324 | let rangeValue; 325 | if (componentProps.componentType === componentTypes.dynamicRangeSlider) { 326 | rangeValue = store.aggregations[`${component}__range__internal`]; 327 | if (componentProps.nestedField) { 328 | rangeValue 329 | = rangeValue 330 | && store.aggregations[`${component}__range__internal`][ 331 | componentProps.nestedField 332 | ].min 333 | ? { 334 | start: store.aggregations[`${component}__range__internal`][componentProps.nestedField].min.value, 335 | end: store.aggregations[`${component}__range__internal`][componentProps.nestedField].max.value, 336 | } // prettier-ignore 337 | : null; 338 | } else { 339 | rangeValue 340 | = rangeValue 341 | && store.aggregations[`${component}__range__internal`].min 342 | && store.aggregations[`${component}__range__internal`].min.value 343 | ? { 344 | start: store.aggregations[`${component}__range__internal`].min.value, 345 | end: store.aggregations[`${component}__range__internal`].max.value, 346 | } // prettier-ignore 347 | : null; 348 | } 349 | } else { 350 | rangeValue = componentProps.range; 351 | } 352 | if (rangeValue) { 353 | if (isValidDateRangeQueryFormat(componentProps.queryFormat)) { 354 | // check if date types are dealt with 355 | range = { 356 | start: formatDate(dayjs(rangeValue.start), componentProps), 357 | end: formatDate(dayjs(rangeValue.end), componentProps), 358 | }; 359 | } else { 360 | range = { 361 | start: parseFloat(rangeValue.start), 362 | end: parseFloat(rangeValue.end), 363 | }; 364 | } 365 | } 366 | } 367 | 368 | // handle date components 369 | if (dateRangeComponents.includes(componentProps.componentType)) { 370 | // Set value 371 | if (value) { 372 | if (isValidDateRangeQueryFormat(componentProps.queryFormat)) { 373 | if (typeof value === 'string') { 374 | value = { 375 | // value would be an ISO Date string 376 | start: formatDate(dayjs(value).subtract(24, 'hour'), componentProps), 377 | end: formatDate(dayjs(value), componentProps), 378 | }; 379 | } else if (Array.isArray(value)) { 380 | value = value.map(val => ({ 381 | // value would be one of ISO Date string, number, native date 382 | start: formatDate(dayjs(val).subtract(24, 'hour'), componentProps), 383 | end: formatDate(dayjs(val), componentProps), 384 | })); 385 | } else { 386 | value = { 387 | start: formatDate( 388 | dayjs(value.start).subtract(24, 'hour'), 389 | componentProps, 390 | ), 391 | end: formatDate(dayjs(value.end), componentProps), 392 | }; 393 | } 394 | } 395 | } 396 | } 397 | } 398 | if (queryType === queryTypes.geo) { 399 | // override the value extracted from selectedValues reducer 400 | value = undefined; 401 | const geoCalcValues 402 | = store.selectedValues[component] 403 | || store.internalValues[component] 404 | || store.internalValues[getInternalComponentID(component)]; 405 | if (geoCalcValues && geoCalcValues.meta) { 406 | if (geoCalcValues.meta.distance && geoCalcValues.meta.coordinates) { 407 | value = { 408 | distance: geoCalcValues.meta.distance, 409 | location: geoCalcValues.meta.coordinates, 410 | }; 411 | if (componentProps.unit) { 412 | value.unit = componentProps.unit; 413 | } 414 | } 415 | if ( 416 | geoCalcValues.meta.mapBoxBounds 417 | && geoCalcValues.meta.mapBoxBounds.top_left 418 | && geoCalcValues.meta.mapBoxBounds.bottom_right 419 | ) { 420 | value = { 421 | // Note: format will be reverse of what we're using now 422 | geoBoundingBox: { 423 | topLeft: `${geoCalcValues.meta.mapBoxBounds.top_left[1]}, ${geoCalcValues.meta.mapBoxBounds.top_left[0]}`, 424 | bottomRight: `${geoCalcValues.meta.mapBoxBounds.bottom_right[1]}, ${geoCalcValues.meta.mapBoxBounds.bottom_right[0]}`, 425 | }, 426 | }; 427 | } 428 | } 429 | } 430 | // handle number box, number box query changes based on the `queryFormat` value 431 | if (componentProps.componentType === componentTypes.numberBox) { 432 | if (queryFormat === 'exact') { 433 | type = 'term'; 434 | } else { 435 | type = 'range'; 436 | if (queryFormat === 'lte') { 437 | value = { 438 | end: value, 439 | boost: 2.0, 440 | }; 441 | } else { 442 | value = { 443 | start: value, 444 | boost: 2.0, 445 | }; 446 | } 447 | } 448 | // Remove query format 449 | queryFormat = 'or'; 450 | } 451 | // Fake dataField for ReactiveComponent 452 | // TODO: Remove it after some time. The `dataField` is no longer required 453 | if (componentProps.componentType === componentTypes.reactiveComponent) { 454 | // Set the type to `term` 455 | type = 'term'; 456 | dataField = 'reactive_component_field'; 457 | // Don't set value property for ReactiveComponent 458 | // since it is driven by `defaultQuery` and `customQuery` 459 | value = undefined; 460 | } 461 | // Assign default value as an empty string for search components so search relevancy can work 462 | if (isSearchComponent(componentProps.componentType) && !value) { 463 | value = ''; 464 | } 465 | 466 | // Handle components which uses label instead of value as the selected value 467 | if (isComponentUsesLabelAsValue(componentProps.componentType)) { 468 | const { data, selectAllLabel } = componentProps; 469 | let absValue = []; 470 | if (value && Array.isArray(value)) { 471 | absValue = value; 472 | } else if (value && typeof value === 'string') { 473 | absValue = [value]; 474 | } 475 | let normalizedValue = []; 476 | if (absValue.length) { 477 | if (data && Array.isArray(data)) { 478 | absValue.forEach((val) => { 479 | const dataItem = data.find(o => o.label === val); 480 | if (dataItem && dataItem.value) { 481 | normalizedValue.push(dataItem.value); 482 | } 483 | }); 484 | } 485 | } 486 | if (selectAllLabel && absValue.length && absValue.includes(selectAllLabel)) { 487 | normalizedValue = absValue; 488 | } 489 | if (normalizedValue.length) { 490 | value = normalizedValue; 491 | } else { 492 | value = undefined; 493 | } 494 | } 495 | if (componentProps.componentType === componentTypes.reactiveList) { 496 | // We set selected page as the value in the redux store for RL. 497 | // It's complex to change this logic in the component so changed it here. 498 | if (value > 0) { 499 | from = (value - 1) * (componentProps.size || 10); 500 | } 501 | value = undefined; 502 | } 503 | let queryValue = value || undefined; 504 | if (componentProps.componentType === componentTypes.searchBox) { 505 | if (Array.isArray(queryValue)) { 506 | queryValue = undefined; 507 | } 508 | } 509 | let endpoint; 510 | if (componentProps.endpoint instanceof Object) { 511 | endpoint = { ...(endpoint || {}), ...componentProps.endpoint }; 512 | } 513 | return { 514 | ...componentProps, 515 | endpoint, 516 | calendarInterval, 517 | dataField, 518 | queryFormat, 519 | type, 520 | aggregations, 521 | interval, 522 | react: store.dependencyTree ? store.dependencyTree[component] : undefined, 523 | customQuery: store.customQueries ? store.customQueries[component] : undefined, 524 | defaultQuery: store.defaultQueries ? store.defaultQueries[component] : undefined, 525 | customHighlight: store.customHighlightOptions 526 | ? store.customHighlightOptions[component] 527 | : undefined, 528 | categoryValue: store.internalValues[component] 529 | ? store.internalValues[component].category 530 | : undefined, 531 | value: queryValue, 532 | pagination, 533 | from, 534 | range, 535 | ...customOptions, 536 | }; 537 | }; 538 | 539 | export function flatReactProp(reactProp, componentID) { 540 | let flattenReact = []; 541 | const flatReact = (react) => { 542 | if (react && Object.keys(react)) { 543 | Object.keys(react).forEach((r) => { 544 | if (react[r]) { 545 | if (typeof react[r] === 'string') { 546 | flattenReact = [...flattenReact, react[r]]; 547 | } else if (Array.isArray(react[r])) { 548 | flattenReact = [...flattenReact, ...react[r]]; 549 | } else if (typeof react[r] === 'object') { 550 | flatReact(react[r]); 551 | } 552 | } 553 | }); 554 | } 555 | }; 556 | flatReact(reactProp); 557 | // Remove cyclic dependencies 558 | flattenReact = flattenReact.filter(react => react !== componentID); 559 | return flattenReact; 560 | } 561 | 562 | 563 | export const getDependentQueries = (store, componentID, orderOfQueries = []) => { 564 | const finalQuery = {}; 565 | const react = flatReactProp(store.dependencyTree[componentID], componentID); 566 | react.forEach((componentObject) => { 567 | const component = componentObject; 568 | const customQuery = store.customQueries[component]; 569 | if (!isInternalComponent(component)) { 570 | const calcValues = store.selectedValues[component] || store.internalValues[component]; 571 | const imageValue = calcValues && calcValues.meta && calcValues.meta.imageValue; 572 | // Only include queries for that component that has `customQuery` or `value` or 573 | // `imageValue` incase of searchbox defined 574 | if (((calcValues && (calcValues.value || imageValue)) || customQuery) 575 | && !finalQuery[component]) { 576 | let execute = false; 577 | const componentProps = store.props[component]; 578 | if ( 579 | Array.isArray(orderOfQueries) 580 | && orderOfQueries.includes(component) 581 | && !( 582 | componentProps.componentType === componentTypes.searchBox 583 | /** 584 | * We want to fire a search query, 585 | * when enableAI 586 | * OR 587 | * when autosuggest is false 588 | */ 589 | && (componentProps.enableAI 590 | || componentProps.autosuggest === false) 591 | ) 592 | ) { 593 | execute = true; 594 | } 595 | // build query 596 | const dependentQuery = getRSQuery( 597 | component, 598 | extractPropsFromState(store, component, { 599 | ...(componentProps && { 600 | ...(componentProps.componentType === componentTypes.searchBox 601 | ? { 602 | ...(execute === false ? { type: queryTypes.search } : {}), 603 | ...(calcValues.category 604 | ? { categoryValue: calcValues.category } 605 | : { categoryValue: undefined }), 606 | ...(calcValues.value ? { value: calcValues.value } : {}), 607 | ...(imageValue 608 | ? { 609 | imageValue, 610 | } : {}), 611 | } 612 | : {}), 613 | ...(componentProps.componentType === componentTypes.categorySearch 614 | ? { 615 | ...(calcValues.category 616 | ? { categoryValue: calcValues.category } 617 | : { categoryValue: undefined }), 618 | } 619 | : {}), 620 | }), 621 | }), 622 | execute, 623 | ); 624 | 625 | if (dependentQuery) { 626 | finalQuery[component] = dependentQuery; 627 | } 628 | } 629 | } 630 | }); 631 | return finalQuery; 632 | }; 633 | 634 | export const transformValueToComponentStateFormat = (value, componentProps) => { 635 | const { componentType, data, queryFormat } = componentProps; 636 | let transformedValue = value; 637 | const meta = {}; 638 | 639 | if (value) { 640 | switch (componentType) { 641 | case componentTypes.singleDataList: 642 | case componentTypes.tabDataList: 643 | transformedValue = ''; 644 | if (Array.isArray(value) && typeof value[0] === 'string') { 645 | transformedValue = value[0]; 646 | } else if (typeof value === 'object' && value.label) { 647 | transformedValue = value.label; 648 | } else { 649 | transformedValue = value; 650 | } 651 | 652 | break; 653 | case componentTypes.multiDataList: 654 | transformedValue = []; 655 | if (Array.isArray(value)) { 656 | value.forEach((valObj) => { 657 | if (typeof valObj === 'object' && (valObj.label || valObj.value)) { 658 | transformedValue.push(valObj.label || valObj.value); 659 | } else if (typeof valObj === 'string') { 660 | transformedValue.push(valObj); 661 | } 662 | }); 663 | } 664 | 665 | break; 666 | case componentTypes.toggleButton: 667 | transformedValue = []; // array of objects 668 | 669 | if (Array.isArray(value)) { 670 | value.forEach((valObj) => { 671 | if (typeof valObj === 'object' && valObj.label && valObj.value) { 672 | transformedValue.push(valObj); 673 | } else if (typeof valObj === 'string') { 674 | const findDataObj = data.find(item => 675 | item.label.trim() === valObj.trim() 676 | || item.value.trim() === valObj.trim()); 677 | transformedValue.push(findDataObj); 678 | } 679 | }); 680 | } else if (typeof value === 'object' && value.label && value.value) { 681 | transformedValue = value.value; 682 | } else if (typeof value === 'string') { 683 | const findDataObj = data.find(item => 684 | item.label.trim() === value.trim() 685 | || item.value.trim() === value.trim()); 686 | transformedValue = findDataObj.value; 687 | } 688 | break; 689 | case componentTypes.singleRange: 690 | case componentTypes.singleDropdownRange: 691 | transformedValue = {}; 692 | 693 | if (!Array.isArray(value) && typeof value === 'object') { 694 | transformedValue = { ...value }; 695 | } else if (typeof value === 'string') { 696 | const findDataObj = data.find(item => item.label.trim() === value.trim()); 697 | transformedValue = { ...findDataObj }; 698 | } 699 | 700 | break; 701 | case componentTypes.multiDropdownRange: 702 | case componentTypes.multiRange: 703 | transformedValue = []; // array of objects 704 | 705 | if (Array.isArray(value)) { 706 | value.forEach((valObj) => { 707 | if ( 708 | typeof valObj === 'object' 709 | && typeof valObj.start === 'number' 710 | && typeof valObj.end === 'number' 711 | ) { 712 | let findDataObj = { ...valObj }; 713 | if (!findDataObj.label) { 714 | findDataObj = data.find(item => 715 | item.start === valObj.start && item.end === valObj.end); 716 | } 717 | transformedValue.push(findDataObj); 718 | } else if (typeof valObj === 'string') { 719 | const findDataObj = data.find(item => item.label.trim() === valObj.trim()); 720 | transformedValue.push(findDataObj); 721 | } 722 | }); 723 | } else if (typeof value === 'string') { 724 | const findDataObj = data.find(item => item.label.trim() === value.trim()); 725 | transformedValue.push(findDataObj); 726 | } 727 | break; 728 | case componentTypes.rangeSlider: 729 | case componentTypes.ratingsFilter: 730 | case componentTypes.dynamicRangeSlider: 731 | case componentTypes.reactiveChart: 732 | transformedValue = []; 733 | if (queryFormat) { 734 | if (Array.isArray(value)) { 735 | transformedValue = value.map(item => 736 | formatDate(dayjs(item), componentProps)); 737 | } else if (typeof value === 'object') { 738 | transformedValue = [ 739 | formatDate(dayjs(value.start), componentProps), 740 | formatDate(dayjs(value.end), componentProps), 741 | ]; 742 | } 743 | } else if (Array.isArray(value)) { 744 | transformedValue = [...value]; 745 | } else if (typeof value === 'object') { 746 | transformedValue = [value.start, value.end]; 747 | } else { 748 | transformedValue = value; 749 | } 750 | break; 751 | case componentTypes.numberBox: 752 | transformedValue = []; 753 | 754 | if (!Array.isArray(value) && typeof value === 'object') { 755 | transformedValue = value.start; 756 | } else if (typeof value === 'number') { 757 | transformedValue = value; 758 | } 759 | break; 760 | case componentTypes.datePicker: 761 | transformedValue = ''; 762 | if (typeof value !== 'object') { 763 | transformedValue = dayjs(value).format('YYYY-MM-DD'); 764 | } else if (value.end) { 765 | transformedValue = dayjs(value.end).format('YYYY-MM-DD'); 766 | } else if (value.start) { 767 | transformedValue = dayjs(value.start).add(24, 'hour').format('YYYY-MM-DD'); 768 | } 769 | break; 770 | case componentTypes.dateRange: 771 | transformedValue = []; // array of strings 772 | if (Array.isArray(value)) { 773 | transformedValue = value.map(t => dayjs(t).format('YYYY-MM-DD')); 774 | } else if (typeof value === 'object') { 775 | transformedValue = [ 776 | dayjs(value.start).format('YYYY-MM-DD'), 777 | dayjs(value.end).format('YYYY-MM-DD'), 778 | ]; 779 | } 780 | break; 781 | case componentTypes.categorySearch: 782 | transformedValue = ''; 783 | if (typeof value === 'object') { 784 | transformedValue = value.value; 785 | if (value.category !== undefined) { 786 | meta.category = value.category; 787 | } 788 | } else if (typeof value === 'string') { 789 | transformedValue = value; 790 | } 791 | break; 792 | default: 793 | break; 794 | } 795 | } 796 | return { value: transformedValue, meta }; 797 | }; 798 | --------------------------------------------------------------------------------