reducer.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import { createSlice, PayloadAction } from '@reduxjs/toolkit';
  2. import { cloneDeep, isString, trim } from 'lodash';
  3. import { applyStateChanges } from '../../../../core/utils/applyStateChanges';
  4. import { ALL_VARIABLE_VALUE } from '../../constants';
  5. import { isMulti, isQuery } from '../../guard';
  6. import { VariableOption, VariableWithOptions } from '../../types';
  7. import { containsSearchFilter } from '../../utils';
  8. export interface ToggleOption {
  9. option?: VariableOption;
  10. forceSelect: boolean;
  11. clearOthers: boolean;
  12. }
  13. export interface OptionsPickerState {
  14. id: string;
  15. selectedValues: VariableOption[];
  16. queryValue: string;
  17. highlightIndex: number;
  18. options: VariableOption[];
  19. multi: boolean;
  20. }
  21. export const initialOptionPickerState: OptionsPickerState = {
  22. id: '',
  23. highlightIndex: -1,
  24. queryValue: '',
  25. selectedValues: [],
  26. options: [],
  27. multi: false,
  28. };
  29. export const OPTIONS_LIMIT = 1000;
  30. const optionsToRecord = (options: VariableOption[]): Record<string, VariableOption> => {
  31. if (!Array.isArray(options)) {
  32. return {};
  33. }
  34. return options.reduce((all: Record<string, VariableOption>, option) => {
  35. if (isString(option.value)) {
  36. all[option.value] = option;
  37. }
  38. return all;
  39. }, {});
  40. };
  41. const updateOptions = (state: OptionsPickerState): OptionsPickerState => {
  42. if (!Array.isArray(state.options)) {
  43. state.options = [];
  44. return state;
  45. }
  46. const selectedOptions = optionsToRecord(state.selectedValues);
  47. state.selectedValues = Object.values(selectedOptions);
  48. state.options = state.options.map((option) => {
  49. if (!isString(option.value)) {
  50. return option;
  51. }
  52. const selected = !!selectedOptions[option.value];
  53. if (option.selected === selected) {
  54. return option;
  55. }
  56. return { ...option, selected };
  57. });
  58. state.options = applyLimit(state.options);
  59. return state;
  60. };
  61. const applyLimit = (options: VariableOption[]): VariableOption[] => {
  62. if (!Array.isArray(options)) {
  63. return [];
  64. }
  65. if (options.length <= OPTIONS_LIMIT) {
  66. return options;
  67. }
  68. return options.slice(0, OPTIONS_LIMIT);
  69. };
  70. const updateDefaultSelection = (state: OptionsPickerState): OptionsPickerState => {
  71. const { options, selectedValues } = state;
  72. if (options.length === 0 || selectedValues.length > 0) {
  73. return state;
  74. }
  75. if (!options[0] || options[0].value !== ALL_VARIABLE_VALUE) {
  76. return state;
  77. }
  78. state.selectedValues = [{ ...options[0], selected: true }];
  79. return state;
  80. };
  81. const updateAllSelection = (state: OptionsPickerState): OptionsPickerState => {
  82. const { selectedValues } = state;
  83. if (selectedValues.length > 1) {
  84. state.selectedValues = selectedValues.filter((option) => option.value !== ALL_VARIABLE_VALUE);
  85. }
  86. return state;
  87. };
  88. const optionsPickerSlice = createSlice({
  89. name: 'templating/optionsPicker',
  90. initialState: initialOptionPickerState,
  91. reducers: {
  92. showOptions: (state, action: PayloadAction<VariableWithOptions>): OptionsPickerState => {
  93. const { query, options } = action.payload;
  94. state.highlightIndex = -1;
  95. state.options = cloneDeep(options);
  96. state.id = action.payload.id;
  97. state.queryValue = '';
  98. state.multi = false;
  99. if (isMulti(action.payload)) {
  100. state.multi = action.payload.multi ?? false;
  101. }
  102. if (isQuery(action.payload)) {
  103. const { queryValue } = action.payload;
  104. const queryHasSearchFilter = containsSearchFilter(query);
  105. state.queryValue = queryHasSearchFilter && queryValue ? queryValue : '';
  106. }
  107. state.selectedValues = state.options.filter((option) => option.selected);
  108. return applyStateChanges(state, updateDefaultSelection, updateOptions);
  109. },
  110. hideOptions: (state, action: PayloadAction): OptionsPickerState => {
  111. return { ...initialOptionPickerState };
  112. },
  113. toggleOption: (state, action: PayloadAction<ToggleOption>): OptionsPickerState => {
  114. const { option, clearOthers, forceSelect } = action.payload;
  115. const { multi, selectedValues } = state;
  116. if (option) {
  117. const selected = !selectedValues.find((o) => o.value === option.value && o.text === option.text);
  118. if (option.value === ALL_VARIABLE_VALUE || !multi || clearOthers) {
  119. if (selected || forceSelect) {
  120. state.selectedValues = [{ ...option, selected: true }];
  121. } else {
  122. state.selectedValues = [];
  123. }
  124. return applyStateChanges(state, updateDefaultSelection, updateAllSelection, updateOptions);
  125. }
  126. if (forceSelect || selected) {
  127. state.selectedValues.push({ ...option, selected: true });
  128. return applyStateChanges(state, updateDefaultSelection, updateAllSelection, updateOptions);
  129. }
  130. state.selectedValues = selectedValues.filter((o) => o.value !== option.value && o.text !== option.text);
  131. } else {
  132. state.selectedValues = [];
  133. }
  134. return applyStateChanges(state, updateDefaultSelection, updateAllSelection, updateOptions);
  135. },
  136. moveOptionsHighlight: (state, action: PayloadAction<number>): OptionsPickerState => {
  137. let nextIndex = state.highlightIndex + action.payload;
  138. if (nextIndex < 0) {
  139. nextIndex = 0;
  140. } else if (nextIndex >= state.options.length) {
  141. nextIndex = state.options.length - 1;
  142. }
  143. return {
  144. ...state,
  145. highlightIndex: nextIndex,
  146. };
  147. },
  148. toggleAllOptions: (state, action: PayloadAction): OptionsPickerState => {
  149. if (state.selectedValues.length > 0) {
  150. state.selectedValues = [];
  151. return applyStateChanges(state, updateOptions);
  152. }
  153. state.selectedValues = state.options
  154. .filter((option) => option.value !== ALL_VARIABLE_VALUE)
  155. .map((option) => ({
  156. ...option,
  157. selected: true,
  158. }));
  159. return applyStateChanges(state, updateOptions);
  160. },
  161. updateSearchQuery: (state, action: PayloadAction<string>): OptionsPickerState => {
  162. state.queryValue = action.payload;
  163. return state;
  164. },
  165. updateOptionsAndFilter: (state, action: PayloadAction<VariableOption[]>): OptionsPickerState => {
  166. const searchQuery = trim((state.queryValue ?? '').toLowerCase());
  167. state.options = action.payload.filter((option) => {
  168. const optionsText = option.text ?? '';
  169. const text = Array.isArray(optionsText) ? optionsText.toString() : optionsText;
  170. return text.toLowerCase().indexOf(searchQuery) !== -1;
  171. });
  172. state.highlightIndex = 0;
  173. return applyStateChanges(state, updateDefaultSelection, updateOptions);
  174. },
  175. updateOptionsFromSearch: (state, action: PayloadAction<VariableOption[]>): OptionsPickerState => {
  176. state.options = action.payload;
  177. state.highlightIndex = 0;
  178. return applyStateChanges(state, updateDefaultSelection, updateOptions);
  179. },
  180. cleanPickerState: () => initialOptionPickerState,
  181. },
  182. });
  183. export const {
  184. toggleOption,
  185. showOptions,
  186. hideOptions,
  187. moveOptionsHighlight,
  188. toggleAllOptions,
  189. updateSearchQuery,
  190. updateOptionsAndFilter,
  191. updateOptionsFromSearch,
  192. cleanPickerState,
  193. } = optionsPickerSlice.actions;
  194. export const optionsPickerReducer = optionsPickerSlice.reducer;