"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useItemsState = exports.getSelectedAndUnselectedItems = exports.Actions = void 0; var _eui = require("@elastic/eui"); var _react = require("react"); /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ var ItemState; (function (ItemState) { ItemState["CHECKED"] = "checked"; ItemState["PARTIAL"] = "partial"; ItemState["UNCHECKED"] = "unchecked"; })(ItemState || (ItemState = {})); let Actions; exports.Actions = Actions; (function (Actions) { Actions[Actions["CHECK_ITEM"] = 0] = "CHECK_ITEM"; Actions[Actions["UNCHECK_ITEM"] = 1] = "UNCHECK_ITEM"; Actions[Actions["SET_NEW_STATE"] = 2] = "SET_NEW_STATE"; })(Actions || (exports.Actions = Actions = {})); var ICONS; (function (ICONS) { ICONS["CHECKED"] = "check"; ICONS["PARTIAL"] = "asterisk"; ICONS["UNCHECKED"] = "empty"; })(ICONS || (ICONS = {})); const stateToIconMap = { [ItemState.CHECKED]: ICONS.CHECKED, [ItemState.PARTIAL]: ICONS.PARTIAL, [ItemState.UNCHECKED]: ICONS.UNCHECKED }; /** * The EuiSelectable has two states values for its items: checked="on" for checked items * and check=undefined for unchecked items. Given that our use case needs * to track items that are part in some cases and not part in some others we need * to keep our own state and sync it with the EuiSelectable. Our state is always * the source of true. * * In our state, a item can be in one of the following states: checked, partial, and unchecked. * A checked item is a item that is either common in all cases or has been * checked by the user. A partial item is a item that is available is some of the * selected cases and not available in others. A user can not make a item partial. * A unchecked item is a item that is either unselected by the user or is not available * in all selected cases. * * State transitions: * * partial --> checked * checked --> unchecked * unchecked --> checked * * A dirty item is a item that the user clicked. Because the EuiSelectable * returns all items (items) on each user interaction we need to distinguish items * that the user unselected from items that are not common between all selected cases * and the user did not interact with them. Marking items as dirty help us to do that. * A user to unselect a item needs to fist checked a partial or an unselected item and make it * selected (and dirty). This guarantees that unchecked items will always become dirty at some * point in the past. * * On mount (initial state) the component gets all available items. * The items that are common in all selected cases are marked as checked * and dirty in our state and checked in EuiSelectable state. * The ones that are not common in any of the selected items are * marked as unchecked and not dirty in our state and unchecked in EuiSelectable state. * The items that are common in some of the cases are marked as partial and not dirty * in our state and unchecked in EuiSelectable state. * * When a user interacts with a item the following happens: * a) If the item is unchecked the EuiSelectable marks it as checked and * we change the state of the item as checked and dirty. * b) If the item is partial the EuiSelectable marks it as checked and * we change the state of the item as checked and dirty. * c) If the item is checked the EuiSelectable marks it as unchecked and * we change the state of the item as unchecked and dirty. */ const itemsReducer = (state, action) => { switch (action.type) { case Actions.CHECK_ITEM: const selectedItems = {}; for (const item of action.payload) { selectedItems[item.key] = { key: item.key, itemState: ItemState.CHECKED, dirty: true, icon: ICONS.CHECKED, data: item.data }; } return { ...state, items: { ...state.items, ...selectedItems } }; case Actions.UNCHECK_ITEM: const unSelectedItems = {}; for (const item of action.payload) { unSelectedItems[item.key] = { key: item.key, itemState: ItemState.UNCHECKED, dirty: true, icon: ICONS.UNCHECKED, data: item.data }; } return { ...state, items: { ...state.items, ...unSelectedItems } }; case Actions.SET_NEW_STATE: return { ...action.payload }; default: (0, _eui.assertNever)(action); } }; const getInitialItemsState = ({ items, selectedCases, fieldSelector }) => { const itemCounterMap = createItemsCounterMapping({ selectedCases, fieldSelector }); const totalCases = selectedCases.length; const itemsRecord = {}; const state = { items: itemsRecord, itemCounterMap }; for (const item of items) { var _itemCounterMap$get; const itemsCounter = (_itemCounterMap$get = itemCounterMap.get(item)) !== null && _itemCounterMap$get !== void 0 ? _itemCounterMap$get : 0; const isCheckedItem = itemsCounter === totalCases; const isPartialItem = itemsCounter < totalCases && itemsCounter !== 0; const itemState = isCheckedItem ? ItemState.CHECKED : isPartialItem ? ItemState.PARTIAL : ItemState.UNCHECKED; const icon = getSelectionIcon(itemState); itemsRecord[item] = { key: item, itemState, dirty: isCheckedItem, icon, data: {} }; } return state; }; const createItemsCounterMapping = ({ selectedCases, fieldSelector }) => { const counterMap = new Map(); for (const theCase of selectedCases) { const items = fieldSelector(theCase); for (const item of items) { var _counterMap$get; counterMap.set(item, ((_counterMap$get = counterMap.get(item)) !== null && _counterMap$get !== void 0 ? _counterMap$get : 0) + 1); } } return counterMap; }; const getSelectionIcon = itemState => { return stateToIconMap[itemState]; }; const getSelectedAndUnselectedItems = (newOptions, items) => { const selectedItems = []; const unSelectedItems = []; for (const option of newOptions) { var _option$data2; if (option.checked === 'on') { var _option$data; selectedItems.push({ key: option.key, data: (_option$data = option.data) !== null && _option$data !== void 0 ? _option$data : {} }); } /** * User can only select the "Add new item" item. Because a new item do not have a state yet * we need to ensure that state access is done only by options with state. */ if (!((_option$data2 = option.data) !== null && _option$data2 !== void 0 && _option$data2.newItem) && !option.checked && items[option.key] && items[option.key].dirty) { var _option$data3; unSelectedItems.push({ key: option.key, data: (_option$data3 = option.data) !== null && _option$data3 !== void 0 ? _option$data3 : {} }); } } return { selectedItems, unSelectedItems }; }; exports.getSelectedAndUnselectedItems = getSelectedAndUnselectedItems; const getKeysFromPayload = items => items.map(item => item.key); const stateToPayload = items => Object.keys(items).map(key => ({ key, data: items[key].data })); const useItemsState = ({ items, selectedCases, fieldSelector, itemToSelectableOption, onChangeItems }) => { /** * If react query refetch on the background and fetches new items the component will * rerender but it will not change the state. getInitialItemsState will run only on * mount. This is a desired behaviour because it prevents the list of items for changing * while the user interacts with the selectable. */ const [state, dispatch] = (0, _react.useReducer)(itemsReducer, { items, selectedCases, fieldSelector }, getInitialItemsState); const stateToOptions = (0, _react.useCallback)(() => { const itemsKeys = Object.keys(state.items); return itemsKeys.map(key => { var _convertedItem$label; const convertedItem = itemToSelectableOption({ key, data: state.items[key].data }); return { key, ...(state.items[key].itemState === ItemState.CHECKED ? { checked: 'on' } : {}), 'data-test-subj': `cases-actions-items-edit-selectable-item-${key}`, ...convertedItem, label: (_convertedItem$label = convertedItem.label) !== null && _convertedItem$label !== void 0 ? _convertedItem$label : key, data: { ...(convertedItem === null || convertedItem === void 0 ? void 0 : convertedItem.data), itemIcon: state.items[key].icon } }; }); }, [state.items, itemToSelectableOption]); const onChange = (0, _react.useCallback)(newOptions => { /** * In this function the user has selected and deselected some items. If the user * pressed the "add new item" option it means that needs to add the new item to the list. * Because the label of the "add new item" item is "Add ${searchValue} as a item" we need to * change the label to the same as the item the user entered. The key will always be the * search term (aka the new label). */ const normalizeOptions = newOptions.map(option => { var _option$data4; if ((_option$data4 = option.data) !== null && _option$data4 !== void 0 && _option$data4.newItem) { var _option$key; return { ...option, label: (_option$key = option.key) !== null && _option$key !== void 0 ? _option$key : '' }; } return option; }); const { selectedItems, unSelectedItems } = getSelectedAndUnselectedItems(normalizeOptions, state.items); dispatch({ type: Actions.CHECK_ITEM, payload: selectedItems }); dispatch({ type: Actions.UNCHECK_ITEM, payload: unSelectedItems }); onChangeItems({ selectedItems: getKeysFromPayload(selectedItems), unSelectedItems: getKeysFromPayload(unSelectedItems) }); }, [onChangeItems, state.items]); const onSelectAll = (0, _react.useCallback)(() => { dispatch({ type: Actions.CHECK_ITEM, payload: stateToPayload(state.items) }); onChangeItems({ selectedItems: Object.keys(state.items), unSelectedItems: [] }); }, [onChangeItems, state.items]); const onSelectNone = (0, _react.useCallback)(() => { const unSelectedItems = []; for (const [id, item] of Object.entries(state.items)) { if (item.itemState === ItemState.CHECKED || item.itemState === ItemState.PARTIAL) { unSelectedItems.push({ key: id, data: item.data }); } } dispatch({ type: Actions.UNCHECK_ITEM, payload: unSelectedItems }); onChangeItems({ selectedItems: [], unSelectedItems: getKeysFromPayload(unSelectedItems) }); }, [state.items, onChangeItems]); const options = (0, _react.useMemo)(() => stateToOptions(), [stateToOptions]); const totalSelectedItems = Object.values(state.items).filter(item => item.itemState === ItemState.CHECKED || item.itemState === ItemState.PARTIAL).length; const resetItems = (0, _react.useCallback)(newItems => { const newState = getInitialItemsState({ items: newItems, selectedCases, fieldSelector }); dispatch({ type: Actions.SET_NEW_STATE, payload: newState }); }, [fieldSelector, selectedCases]); return (0, _react.useMemo)(() => ({ state, options, totalSelectedItems, onChange, onSelectAll, onSelectNone, resetItems }), [onChange, onSelectAll, onSelectNone, options, resetItems, state, totalSelectedItems]); }; exports.useItemsState = useItemsState;