searchPanel.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import {useCallback, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import Fuse from 'fuse.js';
  4. import TextField from 'sentry/components/deprecatedforms/textField';
  5. import space from 'sentry/styles/space';
  6. import {IconData, iconGroups, IconPropName, iconProps, icons} from './data';
  7. import IconEntry from './iconEntry';
  8. export type ExtendedIconData = IconData & {
  9. name: string;
  10. defaultProps?: Partial<Record<IconPropName, unknown>>;
  11. };
  12. type Results = {icons: ExtendedIconData[]; id: string; label?: string}[];
  13. export type SelectedIcon = {
  14. group: string;
  15. icon: string;
  16. };
  17. const enumerateIconProps = (iconData: ExtendedIconData[], prop: IconPropName) =>
  18. iconData.reduce<ExtendedIconData[]>((acc, cur) => {
  19. const propData = iconProps[prop];
  20. switch (propData.type) {
  21. case 'select':
  22. const availableOptions = cur.limitOptions?.[prop] ?? propData.options ?? [];
  23. return [
  24. ...acc,
  25. ...availableOptions.map(option => ({
  26. ...cur,
  27. id: `${cur.id}-${prop}-${option.value}`,
  28. defaultProps: {...cur.defaultProps, [prop]: option.value},
  29. })),
  30. ];
  31. case 'boolean':
  32. return [
  33. ...acc,
  34. {...cur, defaultProps: {...cur.defaultProps, [prop]: false}},
  35. {
  36. ...cur,
  37. id: `${cur.id}-${prop}`,
  38. defaultProps: {...cur.defaultProps, [prop]: true},
  39. },
  40. ];
  41. default:
  42. return acc;
  43. }
  44. }, []);
  45. const enumerateIconVariants = (iconData: ExtendedIconData[]): ExtendedIconData[] =>
  46. iconData.reduce<ExtendedIconData[]>((acc, cur) => {
  47. let iconVariants: ExtendedIconData[] = [{...cur, defaultProps: {}}];
  48. cur.additionalProps?.forEach(prop => {
  49. if (iconProps[prop].enumerate) {
  50. iconVariants = enumerateIconProps(iconVariants, prop);
  51. }
  52. });
  53. return [...acc, ...iconVariants];
  54. }, []);
  55. const addIconNames = (iconData: IconData[]): ExtendedIconData[] =>
  56. iconData.map(icon => {
  57. const nameString = icon.id.split('-')[0];
  58. const name = nameString.charAt(0).toUpperCase() + nameString.slice(1);
  59. return {...icon, name};
  60. });
  61. // All the icons, split into iterable groups
  62. const groupedIcons: Results = iconGroups.map(group => {
  63. const filteredIcons = icons.filter(i => i.groups.includes(group.id));
  64. const namedIcons = addIconNames(filteredIcons);
  65. const enumeratedIcons = enumerateIconVariants(namedIcons);
  66. return {...group, icons: enumeratedIcons};
  67. });
  68. const fuse = new Fuse(icons, {keys: ['id', 'groups', 'keywords'], threshold: 0.3});
  69. function SearchPanel() {
  70. /**
  71. * Use Fuse.js to implement icon search
  72. */
  73. const [query, setQuery] = useState('');
  74. const [results, setResults] = useState<Results>(groupedIcons);
  75. const debouncedSearch = useCallback((newQuery: string) => {
  76. if (!newQuery) {
  77. setResults(groupedIcons);
  78. } else {
  79. const searchResults = fuse.search(newQuery).map(result => result.item);
  80. const namedIcons = addIconNames(searchResults);
  81. const enumeratedIcons = enumerateIconVariants(namedIcons);
  82. setResults([{id: 'search', icons: enumeratedIcons}]);
  83. }
  84. }, []);
  85. useEffect(() => void debouncedSearch(query), [query, debouncedSearch]);
  86. return (
  87. <Wrap>
  88. <TextField
  89. name="query"
  90. placeholder="Search icons by name or similar keywords"
  91. value={query}
  92. onChange={value => {
  93. setQuery(value as string);
  94. }}
  95. />
  96. {results.map(group => (
  97. <GroupWrap key={group.id}>
  98. <GroupLabel>{group.label}</GroupLabel>
  99. <GroupIcons>
  100. {group.icons.map(icon => (
  101. <IconEntry key={icon.id} icon={icon} />
  102. ))}
  103. </GroupIcons>
  104. </GroupWrap>
  105. ))}
  106. </Wrap>
  107. );
  108. }
  109. export default SearchPanel;
  110. export const Wrap = styled('div')`
  111. margin-top: ${space(4)};
  112. `;
  113. const GroupWrap = styled('div')`
  114. margin: ${space(4)} 0;
  115. `;
  116. const GroupLabel = styled('p')`
  117. font-size: ${p => p.theme.fontSizeExtraLarge};
  118. font-weight: bold;
  119. margin-bottom: 0;
  120. `;
  121. const GroupIcons = styled('div')`
  122. display: grid;
  123. grid-template-columns: repeat(4, 1fr);
  124. gap: ${space(1)};
  125. margin-top: ${space(1)};
  126. `;