UNPKG

d2-ui

Version:
184 lines (157 loc) 6.82 kB
import { Observable } from 'rxjs'; import log from 'loglevel'; import { uniqBy } from 'lodash/fp'; import { curry } from 'lodash/fp'; import { get } from 'lodash/fp'; import Action from '../../action/Action'; import Store from '../../store/Store'; import addDeepLinksForMaintenance from './sources/maintenance-app'; import addDeepLinksForSettings from './sources/settings-app'; import headerBarStore$ from '../headerBar.store'; import { appsMenuItems$ } from '../headerBar.store'; const identity = v => v; const searchResultBoxStateStore$ = Store.create({ getInitialState() { return { isSearchFieldFocused: false, open: false, searchValue: '', selected: 0, searchResults: [], }; } }); const getParentApp = get('parentApp'); const hasParentApp = item => !!getParentApp(item); const uniqueByName = uniqBy(item => item.name); const filterByValue = curry((searchTerms, item) => searchTerms.every(term => item.label.toLowerCase().includes(term))); const isFullApp = item => !hasParentApp(item); const isNotAFullApp = item => !isFullApp(item); // Only allow deep links for apps for which the user has access to the parentApp const hasAvailableFullApp = curry((fullApps, item) => fullApps.some(app => app.name === item.parentApp)); export function setSearchValue(searchValue) { const matchesSearchValue = filterByValue(searchValue.trim().toLowerCase().split(/\s+/)); searchSourceStore$ .take(1) .subscribe((searchResults) => { const fullApps = searchResults.filter(isFullApp); const fullAppsThatMatchSearchString = fullApps.filter(matchesSearchValue); const deepLinksThatMatchSearchString = searchResults .filter(matchesSearchValue) .filter(isNotAFullApp) .filter(hasAvailableFullApp(fullApps)); // Determine which parent apps we need to show at the end of the list. // When we have deep links in the search results we should also shown their parent app. const parentAppsForMatchedItems = fullApps .filter(item => deepLinksThatMatchSearchString .map(getParentApp) .some(parentApp => parentApp === item.name), ); // Combine all results // - Full applications that match the search string // - Deep links that match the search string // - Full apps for deep links that match the search string // As it might be possible that Full apps are in the results twice we only show the first one // by running the result list through unique by name. const allSearchResults = uniqueByName([].concat( fullAppsThatMatchSearchString, deepLinksThatMatchSearchString, parentAppsForMatchedItems), ); searchResultBoxStateStore$.setState({ ...searchResultBoxStateStore$.getState(), searchResults: allSearchResults, searchValue, }); }); } export function setHovering(isHoveringOverResults) { searchResultBoxStateStore$.setState({ ...searchResultBoxStateStore$.getState(), isHoveringOverResults, }); } export function setSearchFieldFocusTo(value) { searchResultBoxStateStore$.setState({ ...searchResultBoxStateStore$.getState(), isSearchFieldFocused: value, }); } function setSelectedIndex(selected) { const numberOfItems = searchResultBoxStateStore$.getState().searchResults.length; if (searchResultBoxStateStore$.getState().selected + selected >= numberOfItems) { return; } if (searchResultBoxStateStore$.getState().selected + selected < 0) { return; } searchResultBoxStateStore$.setState({ ...searchResultBoxStateStore$.getState(), selected: searchResultBoxStateStore$.getState().selected + selected, }); } export function hideWhenNotHovering() { if (searchResultBoxStateStore$.getState() && !searchResultBoxStateStore$.getState().isHoveringOverResults) { setSearchFieldFocusTo(false); } } export const search = Action.create('Search Apps'); search .map(action => action.data || '') .subscribe(setSearchValue); const searchSourceStore$ = headerBarStore$ .map(headerBarState => [].concat(headerBarState.appItems, headerBarState.profileItems)) .flatMap(addDeepLinksForMaintenance) .flatMap(addDeepLinksForSettings); export const searchStore$ = Observable .combineLatest( searchResultBoxStateStore$, appsMenuItems$, (searchResult, appsMenuItems) => { if (!searchResult.searchValue) { return { ...searchResult, searchResults: appsMenuItems, }; } return searchResult; }, ) .map(resultState => ({ ...resultState, searchResults: resultState.searchResults .map((item, index) => Object.assign({}, item, { selected: resultState.selected === index })), open: Boolean(resultState.isSearchFieldFocused), })); export const handleKeyPress = Action.create(); const keyPress$ = handleKeyPress .map(action => action.data); // Handle an enter key press to go the location of the first item keyPress$ .filter(([event]) => event.keyCode === 13 || event.key === 'Enter') .flatMap(() => searchResultBoxStateStore$.take(1)) // Find the selected menu item in the search results list by the `selected` index .map(state => state.searchResults.find((item, index) => index === state.selected)) .filter(identity) .subscribe( itemToGoTo => window.location = itemToGoTo.action, log.error, ); // When the right arrow is pressed move the selected item to the next one keyPress$ .map(actionData => actionData[0]) .filter(event => event.keyCode === 39 || event.key === 'ArrowRight') .subscribe(() => setSelectedIndex(1)); // When the left arrow is pressed move the selected item to the next one keyPress$ .map(actionData => actionData[0]) .filter(event => event.keyCode === 37 || event.key === 'ArrowLeft') .subscribe(() => setSelectedIndex(-1)); // When the left arrow is pressed move the selected item to the next row keyPress$ .filter(([event, itemsOnRow]) => event.keyCode === 38 || event.key === 'ArrowUp') .subscribe(([_, itemsOnRow]) => setSelectedIndex(-itemsOnRow)); // When the left arrow is pressed move the selected item to the previous row keyPress$ .filter(([event, itemsOnRow]) => event.keyCode === 40 || event.key === 'ArrowDown') .subscribe(([_, itemsOnRow]) => setSelectedIndex(itemsOnRow));