GlobalSearch.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import React, {
  2. FC, useState, useCallback, useRef,
  3. } from 'react';
  4. import assert from 'assert';
  5. import { useTranslation } from 'react-i18next';
  6. import { IFocusable } from '~/client/interfaces/focusable';
  7. import AppContainer from '~/client/services/AppContainer';
  8. import { IPageWithSearchMeta } from '~/interfaces/search';
  9. import { useCurrentPagePath } from '~/stores/context';
  10. import { useGlobalSearchFormRef } from '~/stores/ui';
  11. import SearchForm from '../SearchForm';
  12. import { withUnstatedContainers } from '../UnstatedUtils';
  13. type Props = {
  14. appContainer: AppContainer,
  15. dropup?: boolean,
  16. }
  17. const GlobalSearch: FC<Props> = (props: Props) => {
  18. const { appContainer, dropup } = props;
  19. const { t } = useTranslation();
  20. const globalSearchFormRef = useRef<IFocusable>(null);
  21. useGlobalSearchFormRef(globalSearchFormRef);
  22. const [text, setText] = useState('');
  23. const [isScopeChildren, setScopeChildren] = useState<boolean>(appContainer.getConfig().isSearchScopeChildrenAsDefault);
  24. const [isFocused, setFocused] = useState<boolean>(false);
  25. const { data: currentPagePath } = useCurrentPagePath();
  26. const gotoPage = useCallback((data: IPageWithSearchMeta[]) => {
  27. assert(data.length > 0);
  28. const page = data[0].data; // should be single page selected
  29. // navigate to page
  30. if (page != null) {
  31. window.location.href = `/${page._id}`;
  32. }
  33. }, []);
  34. const search = useCallback(() => {
  35. const url = new URL(window.location.href);
  36. url.pathname = '/_search';
  37. // construct search query
  38. let q = text;
  39. if (isScopeChildren) {
  40. q += ` prefix:${currentPagePath ?? window.location.pathname}`;
  41. }
  42. url.searchParams.append('q', q);
  43. window.location.href = url.href;
  44. }, [currentPagePath, isScopeChildren, text]);
  45. const scopeLabel = isScopeChildren
  46. ? t('header_search_box.label.This tree')
  47. : t('header_search_box.label.All pages');
  48. const isSearchServiceReachable = appContainer.getConfig().isSearchServiceReachable;
  49. const isIndicatorShown = !isFocused && (text.length === 0);
  50. return (
  51. <div className={`form-group mb-0 d-print-none ${isSearchServiceReachable ? '' : 'has-error'}`}>
  52. <div className="input-group flex-nowrap">
  53. <div className={`input-group-prepend ${dropup ? 'dropup' : ''}`}>
  54. <button
  55. className="btn btn-secondary dropdown-toggle py-0"
  56. type="button"
  57. data-toggle="dropdown"
  58. aria-haspopup="true"
  59. data-testid="select-search-scope"
  60. >
  61. {scopeLabel}
  62. </button>
  63. <div className="dropdown-menu">
  64. <button
  65. className="dropdown-item"
  66. type="button"
  67. onClick={() => {
  68. setScopeChildren(false);
  69. globalSearchFormRef.current?.focus();
  70. }}
  71. >
  72. { t('header_search_box.item_label.All pages') }
  73. </button>
  74. <button
  75. data-tesid="search-current-tree"
  76. className="dropdown-item"
  77. type="button"
  78. onClick={() => {
  79. setScopeChildren(true);
  80. globalSearchFormRef.current?.focus();
  81. }}
  82. >
  83. { t('header_search_box.item_label.This tree') }
  84. </button>
  85. </div>
  86. </div>
  87. <SearchForm
  88. ref={globalSearchFormRef}
  89. isSearchServiceReachable={isSearchServiceReachable}
  90. dropup={dropup}
  91. onChange={gotoPage}
  92. onBlur={() => setFocused(false)}
  93. onFocus={() => setFocused(true)}
  94. onInputChange={text => setText(text)}
  95. onSubmit={search}
  96. />
  97. { isIndicatorShown && (
  98. <span className="grw-shortcut-key-indicator">
  99. <code className="bg-transparent text-muted">/</code>
  100. </span>
  101. ) }
  102. </div>
  103. </div>
  104. );
  105. };
  106. /**
  107. * Wrapper component for using unstated
  108. */
  109. const GlobalSearchWrapper = withUnstatedContainers(GlobalSearch, [AppContainer]);
  110. export default GlobalSearchWrapper;