@backstage/plugin-search-react 1.1.0-next.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -1,17 +1,20 @@
1
1
  import { createApiRef, AnalyticsContext, useApi, useAnalytics, configApiRef } from '@backstage/core-plugin-api';
2
2
  import React, { useMemo, useContext, useState, useCallback, useEffect, forwardRef, useRef } from 'react';
3
- import { makeStyles, InputBase, InputAdornment, IconButton, CircularProgress, ListItemIcon, ListItemText, TextField, Chip, FormControl, FormLabel, FormControlLabel, Checkbox, InputLabel, Select, MenuItem, Button, ListItem, Box, Divider } from '@material-ui/core';
3
+ import { makeStyles, InputBase, InputAdornment, IconButton, CircularProgress, ListItemIcon, ListItemText, TextField, Chip, FormControl, FormLabel, FormControlLabel, Checkbox, InputLabel, Select, MenuItem, Button, ListItem, Box, Divider, List, Typography, ListSubheader, Menu } from '@material-ui/core';
4
4
  import useDebounce from 'react-use/lib/useDebounce';
5
5
  import SearchIcon from '@material-ui/icons/Search';
6
6
  import ClearButton from '@material-ui/icons/Clear';
7
- import { createVersionedContext, createVersionedValueMap } from '@backstage/version-bridge';
7
+ import { isEqual } from 'lodash';
8
8
  import useAsync from 'react-use/lib/useAsync';
9
9
  import usePrevious from 'react-use/lib/usePrevious';
10
+ import { createVersionedContext, createVersionedValueMap } from '@backstage/version-bridge';
10
11
  import { Autocomplete } from '@material-ui/lab';
11
12
  import useAsyncFn from 'react-use/lib/useAsyncFn';
12
13
  import { Progress, ResponseErrorPanel, EmptyState, Link } from '@backstage/core-components';
13
14
  import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
14
15
  import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
16
+ import qs from 'qs';
17
+ import AddIcon from '@material-ui/icons/Add';
15
18
 
16
19
  const searchApiRef = createApiRef({
17
20
  id: "plugin.search.queryservice"
@@ -25,7 +28,7 @@ class MockSearchApi {
25
28
  }
26
29
  }
27
30
 
28
- const useStyles$2 = makeStyles(
31
+ const useStyles$3 = makeStyles(
29
32
  () => ({
30
33
  highlight: {}
31
34
  }),
@@ -36,7 +39,7 @@ const HighlightedSearchResultText = ({
36
39
  preTag,
37
40
  postTag
38
41
  }) => {
39
- const classes = useStyles$2();
42
+ const classes = useStyles$3();
40
43
  const terms = useMemo(
41
44
  () => text.split(new RegExp(`(${preTag}.+?${postTag})`)),
42
45
  [postTag, preTag, text]
@@ -74,13 +77,14 @@ const searchInitialState = {
74
77
  const useSearchContextValue = (initialValue = searchInitialState) => {
75
78
  var _a, _b, _c, _d;
76
79
  const searchApi = useApi(searchApiRef);
80
+ const [term, setTerm] = useState(initialValue.term);
81
+ const [types, setTypes] = useState(initialValue.types);
82
+ const [filters, setFilters] = useState(initialValue.filters);
77
83
  const [pageCursor, setPageCursor] = useState(
78
84
  initialValue.pageCursor
79
85
  );
80
- const [filters, setFilters] = useState(initialValue.filters);
81
- const [term, setTerm] = useState(initialValue.term);
82
- const [types, setTypes] = useState(initialValue.types);
83
86
  const prevTerm = usePrevious(term);
87
+ const prevFilters = usePrevious(filters);
84
88
  const result = useAsync(
85
89
  () => searchApi.query({
86
90
  term,
@@ -88,7 +92,7 @@ const useSearchContextValue = (initialValue = searchInitialState) => {
88
92
  pageCursor,
89
93
  types
90
94
  }),
91
- [term, filters, types, pageCursor]
95
+ [term, types, filters, pageCursor]
92
96
  );
93
97
  const hasNextPage = !result.loading && !result.error && ((_a = result.value) == null ? void 0 : _a.nextPageCursor);
94
98
  const hasPreviousPage = !result.loading && !result.error && ((_b = result.value) == null ? void 0 : _b.previousPageCursor);
@@ -105,6 +109,11 @@ const useSearchContextValue = (initialValue = searchInitialState) => {
105
109
  setPageCursor(void 0);
106
110
  }
107
111
  }, [term, prevTerm, setPageCursor]);
112
+ useEffect(() => {
113
+ if (prevFilters !== void 0 && !isEqual(filters, prevFilters)) {
114
+ setPageCursor(void 0);
115
+ }
116
+ }, [filters, prevFilters, setPageCursor]);
108
117
  const value = {
109
118
  result,
110
119
  filters,
@@ -467,7 +476,7 @@ const AutocompleteFilter = (props) => {
467
476
  });
468
477
  };
469
478
 
470
- const useStyles$1 = makeStyles({
479
+ const useStyles$2 = makeStyles({
471
480
  label: {
472
481
  textTransform: "capitalize"
473
482
  }
@@ -481,7 +490,7 @@ const CheckboxFilter = (props) => {
481
490
  values: givenValues = [],
482
491
  valuesDebounceMs
483
492
  } = props;
484
- const classes = useStyles$1();
493
+ const classes = useStyles$2();
485
494
  const { filters, setFilters } = useSearch();
486
495
  useDefaultFilterValue(name, defaultValue);
487
496
  const asyncValues = typeof givenValues === "function" ? givenValues : void 0;
@@ -536,7 +545,7 @@ const SelectFilter = (props) => {
536
545
  values: givenValues,
537
546
  valuesDebounceMs
538
547
  } = props;
539
- const classes = useStyles$1();
548
+ const classes = useStyles$2();
540
549
  useDefaultFilterValue(name, defaultValue);
541
550
  const asyncValues = typeof givenValues === "function" ? givenValues : void 0;
542
551
  const defaultValues = typeof givenValues === "function" ? void 0 : givenValues;
@@ -595,39 +604,68 @@ SearchFilter.Autocomplete = (props) => /* @__PURE__ */ React.createElement(Searc
595
604
  component: AutocompleteFilter
596
605
  });
597
606
 
598
- const SearchResultComponent = ({ children }) => {
599
- const {
600
- result: { loading, error, value }
601
- } = useSearch();
602
- if (loading) {
603
- return /* @__PURE__ */ React.createElement(Progress, null);
604
- }
605
- if (error) {
606
- return /* @__PURE__ */ React.createElement(ResponseErrorPanel, {
607
- title: "Error encountered while fetching search results",
608
- error
609
- });
610
- }
611
- if (!(value == null ? void 0 : value.results.length)) {
612
- return /* @__PURE__ */ React.createElement(EmptyState, {
613
- missing: "data",
614
- title: "Sorry, no results were found"
615
- });
616
- }
617
- return /* @__PURE__ */ React.createElement(React.Fragment, null, children({ results: value.results }));
607
+ const SearchResultContext = (props) => {
608
+ const { children } = props;
609
+ const context = useSearch();
610
+ const state = context.result;
611
+ return children(state);
618
612
  };
619
- const HigherOrderSearchResult = (props) => {
620
- return /* @__PURE__ */ React.createElement(AnalyticsContext, {
621
- attributes: {
622
- pluginId: "search",
623
- extension: "SearchResult"
613
+ const SearchResultApi = (props) => {
614
+ const { query, children } = props;
615
+ const searchApi = useApi(searchApiRef);
616
+ const state = useAsync(
617
+ () => {
618
+ var _a, _b, _c;
619
+ return searchApi.query({
620
+ term: (_a = query.term) != null ? _a : "",
621
+ types: (_b = query.types) != null ? _b : [],
622
+ filters: (_c = query.filters) != null ? _c : {},
623
+ pageCursor: query.pageCursor
624
+ });
625
+ },
626
+ [query]
627
+ );
628
+ return children(state);
629
+ };
630
+ const SearchResultState = (props) => {
631
+ const { query, children } = props;
632
+ return query ? /* @__PURE__ */ React.createElement(SearchResultApi, {
633
+ query
634
+ }, children) : /* @__PURE__ */ React.createElement(SearchResultContext, null, children);
635
+ };
636
+ const SearchResultComponent = (props) => {
637
+ const { query, children } = props;
638
+ return /* @__PURE__ */ React.createElement(SearchResultState, {
639
+ query
640
+ }, ({ loading, error, value }) => {
641
+ if (loading) {
642
+ return /* @__PURE__ */ React.createElement(Progress, null);
624
643
  }
625
- }, /* @__PURE__ */ React.createElement(SearchResultComponent, {
626
- ...props
627
- }));
644
+ if (error) {
645
+ return /* @__PURE__ */ React.createElement(ResponseErrorPanel, {
646
+ title: "Error encountered while fetching search results",
647
+ error
648
+ });
649
+ }
650
+ if (!(value == null ? void 0 : value.results.length)) {
651
+ return /* @__PURE__ */ React.createElement(EmptyState, {
652
+ missing: "data",
653
+ title: "Sorry, no results were found"
654
+ });
655
+ }
656
+ return children(value);
657
+ });
628
658
  };
659
+ const SearchResult = (props) => /* @__PURE__ */ React.createElement(AnalyticsContext, {
660
+ attributes: {
661
+ pluginId: "search",
662
+ extension: "SearchResult"
663
+ }
664
+ }, /* @__PURE__ */ React.createElement(SearchResultComponent, {
665
+ ...props
666
+ }));
629
667
 
630
- const useStyles = makeStyles((theme) => ({
668
+ const useStyles$1 = makeStyles((theme) => ({
631
669
  root: {
632
670
  display: "flex",
633
671
  justifyContent: "space-between",
@@ -637,12 +675,12 @@ const useStyles = makeStyles((theme) => ({
637
675
  }));
638
676
  const SearchResultPager = () => {
639
677
  const { fetchNextPage, fetchPreviousPage } = useSearch();
640
- const classes = useStyles();
678
+ const classes = useStyles$1();
641
679
  if (!fetchNextPage && !fetchPreviousPage) {
642
680
  return /* @__PURE__ */ React.createElement(React.Fragment, null);
643
681
  }
644
682
  return /* @__PURE__ */ React.createElement("nav", {
645
- "arial-label": "pagination navigation",
683
+ "aria-label": "pagination navigation",
646
684
  className: classes.root
647
685
  }, /* @__PURE__ */ React.createElement(Button, {
648
686
  "aria-label": "previous page",
@@ -712,5 +750,277 @@ const HigherOrderDefaultResultListItem = (props) => {
712
750
  }));
713
751
  };
714
752
 
715
- export { AutocompleteFilter, CheckboxFilter, HigherOrderDefaultResultListItem as DefaultResultListItem, HighlightedSearchResultText, MockSearchApi, SearchAutocomplete, SearchAutocompleteDefaultOption, SearchBar, SearchBarBase, SearchContextProvider, SearchFilter, HigherOrderSearchResult as SearchResult, SearchResultComponent, SearchResultPager, SelectFilter, searchApiRef, useSearch, useSearchContextCheck };
753
+ const SearchResultListLayout = (props) => {
754
+ const { loading, error, resultItems, renderResultItem, ...rest } = props;
755
+ return /* @__PURE__ */ React.createElement(List, {
756
+ ...rest
757
+ }, loading ? /* @__PURE__ */ React.createElement(Progress, null) : null, !loading && error ? /* @__PURE__ */ React.createElement(ResponseErrorPanel, {
758
+ title: "Error encountered while fetching search results",
759
+ error
760
+ }) : null, !loading && !error && (resultItems == null ? void 0 : resultItems.length) ? resultItems.map((resultItem) => {
761
+ var _a;
762
+ return (_a = renderResultItem == null ? void 0 : renderResultItem(resultItem)) != null ? _a : null;
763
+ }) : null, !loading && !error && !(resultItems == null ? void 0 : resultItems.length) ? /* @__PURE__ */ React.createElement(EmptyState, {
764
+ missing: "data",
765
+ title: "Sorry, no results were found"
766
+ }) : null);
767
+ };
768
+ const SearchResultList = (props) => {
769
+ const {
770
+ query,
771
+ renderResultItem = ({ document }) => /* @__PURE__ */ React.createElement(HigherOrderDefaultResultListItem, {
772
+ key: document.location,
773
+ result: document
774
+ }),
775
+ ...rest
776
+ } = props;
777
+ return /* @__PURE__ */ React.createElement(AnalyticsContext, {
778
+ attributes: {
779
+ pluginId: "search",
780
+ extension: "SearchResultList"
781
+ }
782
+ }, /* @__PURE__ */ React.createElement(SearchResultState, {
783
+ query
784
+ }, ({ loading, error, value }) => /* @__PURE__ */ React.createElement(SearchResultListLayout, {
785
+ ...rest,
786
+ loading,
787
+ error,
788
+ resultItems: value == null ? void 0 : value.results,
789
+ renderResultItem
790
+ })));
791
+ };
792
+
793
+ const useStyles = makeStyles((theme) => ({
794
+ listSubheader: {
795
+ display: "flex",
796
+ alignItems: "center"
797
+ },
798
+ listSubheaderName: {
799
+ marginLeft: theme.spacing(1),
800
+ textTransform: "uppercase"
801
+ },
802
+ listSubheaderChip: {
803
+ color: theme.palette.text.secondary,
804
+ margin: theme.spacing(0, 0, 0, 1.5)
805
+ },
806
+ listSubheaderFilter: {
807
+ display: "flex",
808
+ color: theme.palette.text.secondary,
809
+ margin: theme.spacing(0, 0, 0, 1.5)
810
+ },
811
+ listSubheaderLink: {
812
+ marginLeft: "auto",
813
+ display: "flex",
814
+ alignItems: "center"
815
+ },
816
+ listSubheaderLinkIcon: {
817
+ fontSize: "inherit",
818
+ marginLeft: theme.spacing(0.5)
819
+ }
820
+ }));
821
+ const SearchResultGroupFilterFieldLayout = (props) => {
822
+ const classes = useStyles();
823
+ const { label, children, ...rest } = props;
824
+ return /* @__PURE__ */ React.createElement(Chip, {
825
+ ...rest,
826
+ className: classes.listSubheaderFilter,
827
+ variant: "outlined",
828
+ label: /* @__PURE__ */ React.createElement(React.Fragment, null, label, ": ", children)
829
+ });
830
+ };
831
+ const NullIcon = () => null;
832
+ const useSearchResultGroupTextFilterStyles = makeStyles((theme) => ({
833
+ root: {
834
+ fontSize: "inherit",
835
+ "&:focus": {
836
+ outline: "none",
837
+ background: theme.palette.common.white
838
+ },
839
+ "&:not(:focus)": {
840
+ cursor: "pointer",
841
+ color: theme.palette.primary.main,
842
+ "&:hover": {
843
+ textDecoration: "underline"
844
+ }
845
+ }
846
+ }
847
+ }));
848
+ const SearchResultGroupTextFilterField = (props) => {
849
+ const classes = useSearchResultGroupTextFilterStyles();
850
+ const { label, value = "None", onChange, onDelete } = props;
851
+ const handleChange = useCallback(
852
+ (e) => {
853
+ onChange(e.target.value);
854
+ },
855
+ [onChange]
856
+ );
857
+ return /* @__PURE__ */ React.createElement(SearchResultGroupFilterFieldLayout, {
858
+ label,
859
+ onDelete
860
+ }, /* @__PURE__ */ React.createElement(Typography, {
861
+ role: "textbox",
862
+ component: "span",
863
+ className: classes.root,
864
+ onChange: handleChange,
865
+ contentEditable: true,
866
+ suppressContentEditableWarning: true
867
+ }, value));
868
+ };
869
+ const useSearchResultGroupSelectFilterStyles = makeStyles((theme) => ({
870
+ root: {
871
+ fontSize: "inherit",
872
+ "&:not(:focus)": {
873
+ cursor: "pointer",
874
+ color: theme.palette.primary.main,
875
+ "&:hover": {
876
+ textDecoration: "underline"
877
+ }
878
+ },
879
+ "&:focus": {
880
+ outline: "none"
881
+ },
882
+ "&>div:first-child": {
883
+ padding: 0
884
+ }
885
+ }
886
+ }));
887
+ const SearchResultGroupSelectFilterField = (props) => {
888
+ const classes = useSearchResultGroupSelectFilterStyles();
889
+ const { label, value = "none", onChange, onDelete, children } = props;
890
+ const handleChange = useCallback(
891
+ (e) => {
892
+ onChange(e.target.value);
893
+ },
894
+ [onChange]
895
+ );
896
+ return /* @__PURE__ */ React.createElement(SearchResultGroupFilterFieldLayout, {
897
+ label,
898
+ onDelete
899
+ }, /* @__PURE__ */ React.createElement(Select, {
900
+ className: classes.root,
901
+ value,
902
+ onChange: handleChange,
903
+ input: /* @__PURE__ */ React.createElement(InputBase, null),
904
+ IconComponent: NullIcon
905
+ }, /* @__PURE__ */ React.createElement(MenuItem, {
906
+ value: "none"
907
+ }, "None"), children));
908
+ };
909
+ function SearchResultGroupLayout(props) {
910
+ const classes = useStyles();
911
+ const [anchorEl, setAnchorEl] = useState(null);
912
+ const {
913
+ loading,
914
+ error,
915
+ icon,
916
+ title,
917
+ titleProps = {},
918
+ link,
919
+ linkProps = {},
920
+ filterOptions,
921
+ renderFilterOption,
922
+ filterFields,
923
+ renderFilterField,
924
+ resultItems,
925
+ renderResultItem,
926
+ ...rest
927
+ } = props;
928
+ const handleClick = useCallback((e) => {
929
+ setAnchorEl(e.currentTarget);
930
+ }, []);
931
+ const handleClose = useCallback(() => {
932
+ setAnchorEl(null);
933
+ }, []);
934
+ return /* @__PURE__ */ React.createElement(List, {
935
+ ...rest
936
+ }, /* @__PURE__ */ React.createElement(ListSubheader, {
937
+ className: classes.listSubheader
938
+ }, icon, /* @__PURE__ */ React.createElement(Typography, {
939
+ className: classes.listSubheaderName,
940
+ component: "strong",
941
+ ...titleProps
942
+ }, title), filterOptions ? /* @__PURE__ */ React.createElement(Chip, {
943
+ className: classes.listSubheaderChip,
944
+ component: "button",
945
+ icon: /* @__PURE__ */ React.createElement(AddIcon, null),
946
+ variant: "outlined",
947
+ label: "Add filter",
948
+ "aria-controls": "filters-menu",
949
+ "aria-haspopup": "true",
950
+ onClick: handleClick
951
+ }) : null, filterOptions ? /* @__PURE__ */ React.createElement(Menu, {
952
+ id: "filters-menu",
953
+ anchorEl,
954
+ open: Boolean(anchorEl),
955
+ onClose: handleClose,
956
+ onClick: handleClose,
957
+ keepMounted: true
958
+ }, filterOptions.map(
959
+ (filterOption) => renderFilterOption ? renderFilterOption(filterOption) : /* @__PURE__ */ React.createElement(MenuItem, {
960
+ key: String(filterOption),
961
+ value: String(filterOption)
962
+ }, filterOption)
963
+ )) : null, filterFields == null ? void 0 : filterFields.map(
964
+ (filterField) => {
965
+ var _a;
966
+ return (_a = renderFilterField == null ? void 0 : renderFilterField(filterField)) != null ? _a : null;
967
+ }
968
+ ), /* @__PURE__ */ React.createElement(Link, {
969
+ className: classes.listSubheaderLink,
970
+ to: "/search",
971
+ ...linkProps
972
+ }, link != null ? link : /* @__PURE__ */ React.createElement(React.Fragment, null, "See all", /* @__PURE__ */ React.createElement(ArrowForwardIosIcon, {
973
+ className: classes.listSubheaderLinkIcon
974
+ })))), loading ? /* @__PURE__ */ React.createElement(Progress, null) : null, !loading && error ? /* @__PURE__ */ React.createElement(ResponseErrorPanel, {
975
+ title: "Error encountered while fetching search results",
976
+ error
977
+ }) : null, !loading && !error && (resultItems == null ? void 0 : resultItems.length) ? resultItems.map((resultItem) => {
978
+ var _a;
979
+ return (_a = renderResultItem == null ? void 0 : renderResultItem(resultItem)) != null ? _a : null;
980
+ }) : null, !loading && !error && !(resultItems == null ? void 0 : resultItems.length) ? /* @__PURE__ */ React.createElement(ListItem, null, /* @__PURE__ */ React.createElement(EmptyState, {
981
+ missing: "data",
982
+ title: "Sorry, no results were found"
983
+ })) : null);
984
+ }
985
+ function SearchResultGroup(props) {
986
+ const {
987
+ query,
988
+ linkProps = {},
989
+ renderResultItem = ({ document }) => /* @__PURE__ */ React.createElement(HigherOrderDefaultResultListItem, {
990
+ key: document.location,
991
+ result: document
992
+ }),
993
+ ...rest
994
+ } = props;
995
+ const to = `/search?${qs.stringify(
996
+ {
997
+ query: query.term,
998
+ types: query.types,
999
+ filters: query.filters,
1000
+ pageCursor: query.pageCursor
1001
+ },
1002
+ { arrayFormat: "brackets" }
1003
+ )}`;
1004
+ return /* @__PURE__ */ React.createElement(AnalyticsContext, {
1005
+ attributes: {
1006
+ pluginId: "search",
1007
+ extension: "SearchResultGroup"
1008
+ }
1009
+ }, /* @__PURE__ */ React.createElement(SearchResultState, {
1010
+ query
1011
+ }, ({ loading, error, value }) => {
1012
+ var _a;
1013
+ return /* @__PURE__ */ React.createElement(SearchResultGroupLayout, {
1014
+ ...rest,
1015
+ loading,
1016
+ error,
1017
+ linkProps: { to, ...linkProps },
1018
+ resultItems: value == null ? void 0 : value.results,
1019
+ renderResultItem,
1020
+ filterFields: Object.keys((_a = query.filters) != null ? _a : {})
1021
+ });
1022
+ }));
1023
+ }
1024
+
1025
+ export { AutocompleteFilter, CheckboxFilter, HigherOrderDefaultResultListItem as DefaultResultListItem, HighlightedSearchResultText, MockSearchApi, SearchAutocomplete, SearchAutocompleteDefaultOption, SearchBar, SearchBarBase, SearchContextProvider, SearchFilter, SearchResult, SearchResultApi, SearchResultComponent, SearchResultContext, SearchResultGroup, SearchResultGroupFilterFieldLayout, SearchResultGroupLayout, SearchResultGroupSelectFilterField, SearchResultGroupTextFilterField, SearchResultList, SearchResultListLayout, SearchResultPager, SearchResultState, SelectFilter, searchApiRef, useSearch, useSearchContextCheck };
716
1026
  //# sourceMappingURL=index.esm.js.map