From 1be957c5f99aff7689e31d008380c171dee40257 Mon Sep 17 00:00:00 2001
From: Min Kim <minkimcello@gmail.com>
Date: Mon, 18 Nov 2024 20:01:01 -0500
Subject: [PATCH] Create search list item component for discussions with links
 to backstage user

---
 .../GithubDiscussionsSearchResultListItem.tsx | 128 ++++++++++++++++++
 .../search/SearchResultCustomList.tsx         | 125 +++++++++--------
 yarn.lock                                     |  25 +++-
 3 files changed, 222 insertions(+), 56 deletions(-)
 create mode 100644 packages/app/src/components/search/GithubDiscussionsSearchResultListItem.tsx

diff --git a/packages/app/src/components/search/GithubDiscussionsSearchResultListItem.tsx b/packages/app/src/components/search/GithubDiscussionsSearchResultListItem.tsx
new file mode 100644
index 0000000..71f2ab2
--- /dev/null
+++ b/packages/app/src/components/search/GithubDiscussionsSearchResultListItem.tsx
@@ -0,0 +1,128 @@
+import React, { ReactNode } from 'react';
+import Box from '@material-ui/core/Box';
+import Chip from '@material-ui/core/Chip';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import Typography from '@material-ui/core/Typography';
+import { makeStyles } from '@material-ui/core/styles';
+import { Link } from '@backstage/core-components';
+import { ResultHighlight } from '@backstage/plugin-search-common';
+import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
+import { IndexableDocument } from '@backstage/plugin-search-common';
+import { EntityRefLink } from '@backstage/plugin-catalog-react';
+
+export interface GithubDiscussionsDocument extends IndexableDocument {
+  author: string;
+  category: string;
+  labels: {
+    name: string;
+    color: string;
+  }[];
+  comments: {
+    author: string;
+    bodyText: string;
+    replies: {
+      author: string;
+      bodyText: string;
+    }[];
+  }[];
+}
+
+const useStyles = makeStyles(theme => ({
+  item: {
+    display: 'flex',
+  },
+  flexContainer: {
+    flexWrap: 'wrap',
+  },
+  itemText: {
+    width: '100%',
+    wordBreak: 'break-all',
+    marginBottom: '1rem',
+  },
+  user: {
+    display: 'inline-flex',
+    margin: theme.spacing(1),
+  },
+}));
+
+/**
+ * Props for {@link GithubDiscussionsSearchResultListItem}.
+ *
+ * @public
+ */
+export interface GithubDiscussionsSearchResultListItemProps {
+  icon?: ReactNode;
+  result?: GithubDiscussionsDocument;
+  highlight?: ResultHighlight;
+  lineClamp?: number;
+}
+
+/** @public */
+export function GithubDiscussionsSearchResultListItem(
+  props: GithubDiscussionsSearchResultListItemProps,
+) {
+  const { result, highlight, icon } = props;
+  const classes = useStyles();
+  if (!result) return null;
+
+  return (
+    <div className={classes.item}>
+      {icon && <ListItemIcon>{icon}</ListItemIcon>}
+      <div className={classes.flexContainer}>
+        <ListItemText
+          className={classes.itemText}
+          primaryTypographyProps={{ variant: 'h6' }}
+          primary={
+            <Link noTrack to={result.location}>
+              {highlight?.fields.title ? (
+                <HighlightedSearchResultText
+                  text={highlight.fields.title}
+                  preTag={highlight.preTag}
+                  postTag={highlight.postTag}
+                />
+              ) : (
+                result.title
+              )}
+            </Link>
+          }
+          secondary={
+            <Typography
+              component="span"
+              style={{
+                display: '-webkit-box',
+                WebkitBoxOrient: 'vertical',
+                WebkitLineClamp: props.lineClamp,
+                overflow: 'hidden',
+              }}
+              color="textSecondary"
+              variant="body2"
+            >
+              {highlight?.fields.text ? (
+                <HighlightedSearchResultText
+                  text={highlight.fields.text}
+                  preTag={highlight.preTag}
+                  postTag={highlight.postTag}
+                />
+              ) : (
+                result.text
+              )}
+            </Typography>
+          }
+        />
+        <Box>
+          {result.author && (
+            <div className={classes.user}>
+              <EntityRefLink entityRef={`user:default/${result.author}`} />
+            </div>
+          )}
+          {result.category && <Chip label={result.category} size="small" />}
+          {result.labels.length > 0 &&
+            result.labels.map(({ name }) => {
+              return <Chip key={name} label={name} size="small" />;
+            })}
+        </Box>
+      </div>
+    </div>
+  );
+}
diff --git a/packages/app/src/components/search/SearchResultCustomList.tsx b/packages/app/src/components/search/SearchResultCustomList.tsx
index 6d0a3ee..918dff0 100644
--- a/packages/app/src/components/search/SearchResultCustomList.tsx
+++ b/packages/app/src/components/search/SearchResultCustomList.tsx
@@ -1,64 +1,81 @@
 import React from 'react';
 
 import { List } from '@material-ui/core';
-import { SearchResult, DefaultResultListItem } from '@backstage/plugin-search-react';
+import {
+  SearchResult,
+  DefaultResultListItem,
+} from '@backstage/plugin-search-react';
 
 import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
-import { StackOverflowSearchResultListItem, StackOverflowIcon } from '@backstage-community/plugin-stack-overflow';
+import {
+  StackOverflowSearchResultListItem,
+  StackOverflowIcon,
+} from '@backstage-community/plugin-stack-overflow';
 import { CatalogIcon, DocsIcon } from '@backstage/core-components';
 import { TechDocsSearchResultCustomListItem } from './TechDocsSearchResultCustomListItem';
+import {
+  GithubDiscussionsSearchResultListItem,
+  GithubDiscussionsDocument,
+} from './GithubDiscussionsSearchResultListItem';
 
 const SearchResultCustomList = () => {
-    return (
-        <SearchResult>
-            {({ results }) => (
-                <List>
-                  {results.map(({ type, document, highlight, rank }) => {
-                    switch (type) {
-                      case 'software-catalog':
-                        return (
-                          <CatalogSearchResultListItem
-                            key={document.location}
-                            result={document}
-                            highlight={highlight}
-                            rank={rank}
-                            icon={<CatalogIcon />}
-                          />
-                        );
-                      case 'techdocs':
-                        return (
-                          <TechDocsSearchResultCustomListItem
-                            key={document.location}
-                            result={document}                            
-                            highlight={highlight}
-                            rank={rank}
-                            asListItem={true}
-                            icon={<DocsIcon />}
-                          />
-                        );
-                      case 'stack-overflow':
-                        return (
-                          <StackOverflowSearchResultListItem
-                            key={document.location}
-                            result={document}
-                            icon={<StackOverflowIcon />}
-                          />
-                        );
-                      default:
-                        return (
-                          <DefaultResultListItem
-                            key={document.location}
-                            result={document}
-                            highlight={highlight}
-                            rank={rank}
-                          />
-                        );
-                    }
-                  })}
-                </List>
-              )}
-        </SearchResult>
-    )
-}
+  return (
+    <SearchResult>
+      {({ results }) => (
+        <List>
+          {results.map(({ type, document, highlight, rank }) => {
+            switch (type) {
+              case 'software-catalog':
+                return (
+                  <CatalogSearchResultListItem
+                    key={document.location}
+                    result={document}
+                    highlight={highlight}
+                    rank={rank}
+                    icon={<CatalogIcon />}
+                  />
+                );
+              case 'techdocs':
+                return (
+                  <TechDocsSearchResultCustomListItem
+                    key={document.location}
+                    result={document}
+                    highlight={highlight}
+                    rank={rank}
+                    asListItem
+                    icon={<DocsIcon />}
+                  />
+                );
+              case 'stack-overflow':
+                return (
+                  <StackOverflowSearchResultListItem
+                    key={document.location}
+                    result={document}
+                    icon={<StackOverflowIcon />}
+                  />
+                );
+              case 'github-discussions':
+                return (
+                  <GithubDiscussionsSearchResultListItem
+                    result={document as GithubDiscussionsDocument}
+                    highlight={highlight}
+                  />
+                );
+              default:
+                return (
+                  <DefaultResultListItem
+                    key={document.location}
+                    result={document}
+                    highlight={highlight}
+                    rank={rank}
+                  />
+                );
+            }
+          })}
+        </List>
+      )}
+    </SearchResult>
+  );
+};
 
-export const searchResultCustomList = <SearchResultCustomList />;
\ No newline at end of file
+export const searchResultCustomList = <SearchResultCustomList />;
diff --git a/yarn.lock b/yarn.lock
index 75594c2..8e5f1e2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -10628,13 +10628,20 @@
   resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
   integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
 
-"@types/react-dom@*", "@types/react-dom@<18.0.0", "@types/react-dom@^18", "@types/react-dom@^18.0.0":
+"@types/react-dom@*", "@types/react-dom@^18.0.0":
   version "18.3.1"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07"
   integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==
   dependencies:
     "@types/react" "*"
 
+"@types/react-dom@<18.0.0":
+  version "17.0.25"
+  resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5"
+  integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA==
+  dependencies:
+    "@types/react" "^17"
+
 "@types/react-redux@^7.1.20":
   version "7.1.34"
   resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e"
@@ -10666,7 +10673,7 @@
   dependencies:
     "@types/react" "*"
 
-"@types/react@*", "@types/react@^16.13.1 || ^17.0.0", "@types/react@^16.13.1 || ^17.0.0 || ^18.0.0", "@types/react@^18":
+"@types/react@*", "@types/react@^16.13.1 || ^17.0.0 || ^18.0.0":
   version "18.3.12"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.12.tgz#99419f182ccd69151813b7ee24b792fe08774f60"
   integrity sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==
@@ -10674,6 +10681,15 @@
     "@types/prop-types" "*"
     csstype "^3.0.2"
 
+"@types/react@^16.13.1 || ^17.0.0", "@types/react@^17":
+  version "17.0.83"
+  resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.83.tgz#b477c56387b74279281149dcf5ba2a1e2216d131"
+  integrity sha512-l0m4ArKJvmFtR4e8UmKrj1pB4tUgOhJITf+mADyF/p69Ts1YAR/E+G9XEM0mHXKVRa1dQNHseyyDNzeuAXfXQw==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/scheduler" "^0.16"
+    csstype "^3.0.2"
+
 "@types/request@^2.47.1", "@types/request@^2.48.8":
   version "2.48.12"
   resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30"
@@ -10701,6 +10717,11 @@
   resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
   integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==
 
+"@types/scheduler@^0.16":
+  version "0.16.8"
+  resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff"
+  integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==
+
 "@types/semver@7.5.8", "@types/semver@^7.5.0":
   version "7.5.8"
   resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"