├── .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 | [](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 |
--------------------------------------------------------------------------------