Browse Source

feat(issues): Display user feedback as view (#79800)

Scott Cooper 4 months ago
parent
commit
21e2a67319

+ 0 - 70
static/app/components/events/userFeedback/userFeedbackDrawer.tsx

@@ -1,70 +0,0 @@
-import styled from '@emotion/styled';
-
-import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
-import {
-  CrumbContainer,
-  EventDrawerBody,
-  EventDrawerContainer,
-  EventDrawerHeader,
-  EventNavigator,
-  Header,
-  NavigationCrumbs,
-  ShortId,
-} from 'sentry/components/events/eventDrawer';
-import {Body} from 'sentry/components/layouts/thirds';
-import {t} from 'sentry/locale';
-import type {Group} from 'sentry/types/group';
-import type {Project} from 'sentry/types/project';
-import {useLocation} from 'sentry/utils/useLocation';
-import useOrganization from 'sentry/utils/useOrganization';
-import usePageFilters from 'sentry/utils/usePageFilters';
-import GroupUserFeedback from 'sentry/views/issueDetails/groupUserFeedback';
-
-export function UserFeedbackDrawer({group, project}: {group: Group; project: Project}) {
-  const location = useLocation();
-  const organization = useOrganization();
-  const {selection} = usePageFilters();
-  const {environments} = selection;
-
-  return (
-    <EventDrawerContainer>
-      <EventDrawerHeader>
-        <NavigationCrumbs
-          crumbs={[
-            {
-              label: (
-                <CrumbContainer>
-                  <ProjectAvatar project={project} />
-                  <ShortId>{group.shortId}</ShortId>
-                </CrumbContainer>
-              ),
-            },
-            {label: t('User Feedback')},
-          ]}
-        />
-      </EventDrawerHeader>
-      <EventNavigator>
-        <Header>{t('User Feedback')}</Header>
-      </EventNavigator>
-      <UserFeedbackBody>
-        <GroupUserFeedback
-          group={group}
-          project={project}
-          location={location}
-          params={{
-            groupId: group.id,
-            orgId: organization.slug,
-          }}
-          environments={environments}
-        />
-      </UserFeedbackBody>
-    </EventDrawerContainer>
-  );
-}
-
-/* Disable grid from Layout styles in drawer */
-const UserFeedbackBody = styled(EventDrawerBody)`
-  ${Body} {
-    grid-template-columns: unset;
-  }
-`;

+ 66 - 0
static/app/views/issueDetails/groupUserFeedback.spec.tsx

@@ -0,0 +1,66 @@
+import {GroupFixture} from 'sentry-fixture/group';
+import {OrganizationFixture} from 'sentry-fixture/organization';
+import {ProjectFixture} from 'sentry-fixture/project';
+
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import ProjectsStore from 'sentry/stores/projectsStore';
+
+import GroupUserFeedback from './groupUserFeedback';
+
+describe('GroupUserFeedback', () => {
+  const group = GroupFixture();
+  const organization = OrganizationFixture();
+  const project = ProjectFixture();
+
+  beforeEach(() => {
+    ProjectsStore.init();
+    ProjectsStore.loadInitialData([project]);
+  });
+
+  it('renders empty state', async () => {
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/issues/${group.id}/user-reports/`,
+      body: [],
+    });
+
+    render(<GroupUserFeedback group={group} />);
+    expect(
+      await screen.findByRole('heading', {
+        name: 'What do users think?',
+      })
+    ).toBeInTheDocument();
+  });
+
+  it('renders user feedback', async () => {
+    MockApiClient.addMockResponse({
+      url: `/organizations/${organization.slug}/issues/${group.id}/user-reports/`,
+      body: [
+        {
+          id: '1111',
+          eventID: 'abc',
+          name: 'Test User',
+          email: 'test@example.com',
+          comments: 'custom comment',
+          dateCreated: '2024-10-24T01:22:30',
+          user: {
+            id: 'something',
+            username: null,
+            email: null,
+            name: null,
+            ipAddress: '127.0.0.1',
+            avatarUrl: null,
+          },
+          event: {
+            id: '123',
+            eventID: 'abc',
+          },
+        },
+      ],
+    });
+
+    render(<GroupUserFeedback group={group} />);
+    expect(await screen.findByText('Test User')).toBeInTheDocument();
+    expect(await screen.findByText('custom comment')).toBeInTheDocument();
+  });
+});

+ 65 - 101
static/app/views/issueDetails/groupUserFeedback.tsx

@@ -1,6 +1,6 @@
-import {Component} from 'react';
+import {Fragment} from 'react';
+import {css} from '@emotion/react';
 import styled from '@emotion/styled';
-import isEqual from 'lodash/isEqual';
 
 import {EventUserFeedback} from 'sentry/components/events/userFeedback';
 import * as Layout from 'sentry/components/layouts/thirds';
@@ -8,99 +8,57 @@ import LoadingError from 'sentry/components/loadingError';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import Pagination from 'sentry/components/pagination';
 import {space} from 'sentry/styles/space';
-import type {Group, UserReport} from 'sentry/types/group';
-import type {RouteComponentProps} from 'sentry/types/legacyReactRouter';
-import type {Organization} from 'sentry/types/organization';
-import type {Project} from 'sentry/types/project';
-import withOrganization from 'sentry/utils/withOrganization';
+import type {Group} from 'sentry/types/group';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import {useGroupUserFeedback} from 'sentry/views/issueDetails/useGroupUserFeedback';
+import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
 import {UserFeedbackEmpty} from 'sentry/views/userFeedback/userFeedbackEmpty';
 
-import {fetchGroupUserReports} from './utils';
-
-type RouteParams = {
-  groupId: string;
-  orgId: string;
-};
-
-type Props = Pick<RouteComponentProps<RouteParams, {}>, 'params' | 'location'> & {
-  environments: string[];
+interface GroupUserFeedbackProps {
   group: Group;
-  organization: Organization;
-  project: Project;
-};
-
-type State = {
-  error: boolean;
-  loading: boolean;
-  reportList: UserReport[];
-  pageLinks?: string | null;
-};
-
-class GroupUserFeedback extends Component<Props, State> {
-  state: State = {
-    loading: true,
-    error: false,
-    reportList: [],
-    pageLinks: '',
-  };
+}
 
-  componentDidMount() {
-    this.fetchData();
+function GroupUserFeedback({group}: GroupUserFeedbackProps) {
+  const organization = useOrganization();
+  const hasStreamlinedUI = useHasStreamlinedUI();
+  const location = useLocation();
+  const {
+    data: reportList,
+    isPending,
+    isError,
+    refetch,
+    getResponseHeader,
+  } = useGroupUserFeedback({
+    groupId: group.id,
+    query: {
+      cursor: location.query.cursor as string | undefined,
+    },
+  });
+
+  if (isError) {
+    return <LoadingError onRetry={refetch} />;
   }
 
-  componentDidUpdate(prevProps: Props) {
-    if (
-      !isEqual(prevProps.params, this.props.params) ||
-      prevProps.location.pathname !== this.props.location.pathname ||
-      prevProps.location.search !== this.props.location.search
-    ) {
-      this.fetchData();
-    }
+  if (isPending) {
+    return (
+      <StyledLayoutBody hasStreamlinedUI={hasStreamlinedUI}>
+        <Layout.Main fullWidth>
+          <LoadingIndicator />
+        </Layout.Main>
+      </StyledLayoutBody>
+    );
   }
 
-  fetchData = () => {
-    const {group, location, organization, params} = this.props;
-    this.setState({
-      loading: true,
-      error: false,
-    });
-
-    fetchGroupUserReports(organization.slug, group.id, {
-      ...params,
-      cursor: location.query.cursor || '',
-    })
-      .then(([data, _, resp]) => {
-        this.setState({
-          error: false,
-          loading: false,
-          reportList: data,
-          pageLinks: resp?.getResponseHeader('Link'),
-        });
-      })
-      .catch(() => {
-        this.setState({
-          error: true,
-          loading: false,
-        });
-      });
-  };
-
-  render() {
-    const {reportList, loading, error} = this.state;
-    const {organization, group} = this.props;
-
-    if (loading) {
-      return <LoadingIndicator />;
-    }
+  const pageLinks = getResponseHeader?.('Link');
 
-    if (error) {
-      return <LoadingError onRetry={this.fetchData} />;
-    }
-
-    if (reportList.length) {
-      return (
-        <Layout.Body>
-          <Layout.Main>
+  return (
+    <StyledLayoutBody hasStreamlinedUI={hasStreamlinedUI}>
+      <Layout.Main fullWidth>
+        {reportList.length === 0 ? (
+          <UserFeedbackEmpty projectIds={[group.project.id]} issueTab />
+        ) : (
+          <Fragment>
             {reportList.map((item, idx) => (
               <StyledEventUserFeedback
                 key={idx}
@@ -109,24 +67,30 @@ class GroupUserFeedback extends Component<Props, State> {
                 issueId={group.id}
               />
             ))}
-            <Pagination pageLinks={this.state.pageLinks} {...this.props} />
-          </Layout.Main>
-        </Layout.Body>
-      );
-    }
-
-    return (
-      <Layout.Body>
-        <Layout.Main fullWidth>
-          <UserFeedbackEmpty projectIds={[group.project.id]} issueTab />
-        </Layout.Main>
-      </Layout.Body>
-    );
-  }
+            <Pagination pageLinks={pageLinks} />
+          </Fragment>
+        )}
+      </Layout.Main>
+    </StyledLayoutBody>
+  );
 }
 
 const StyledEventUserFeedback = styled(EventUserFeedback)`
   margin-bottom: ${space(2)};
 `;
 
-export default withOrganization(GroupUserFeedback);
+const StyledLayoutBody = styled(Layout.Body)<{hasStreamlinedUI?: boolean}>`
+  ${p =>
+    p.hasStreamlinedUI &&
+    css`
+      border: 1px solid ${p.theme.border};
+      border-radius: ${p.theme.borderRadius};
+      padding: ${space(2)} 0;
+
+      @media (min-width: ${p.theme.breakpoints.medium}) {
+        padding: ${space(2)} ${space(2)};
+      }
+    `}
+`;
+
+export default GroupUserFeedback;

+ 0 - 44
static/app/views/issueDetails/streamline/useUserFeedbackDrawer.tsx

@@ -1,44 +0,0 @@
-import {useCallback} from 'react';
-
-import {UserFeedbackDrawer} from 'sentry/components/events/userFeedback/userFeedbackDrawer';
-import useDrawer from 'sentry/components/globalDrawer';
-import {t} from 'sentry/locale';
-import type {Group} from 'sentry/types/group';
-import type {Project} from 'sentry/types/project';
-import {useLocation} from 'sentry/utils/useLocation';
-import {useNavigate} from 'sentry/utils/useNavigate';
-import {useGroupDetailsRoute} from 'sentry/views/issueDetails/useGroupDetailsRoute';
-
-export function useUserFeedbackDrawer({
-  group,
-  project,
-}: {
-  group: Group;
-  project: Project;
-}) {
-  const {openDrawer} = useDrawer();
-  const {baseUrl} = useGroupDetailsRoute();
-  const navigate = useNavigate();
-  const location = useLocation();
-
-  const openUserFeedbackDrawer = useCallback(() => {
-    openDrawer(() => <UserFeedbackDrawer group={group} project={project} />, {
-      ariaLabel: t('User Feedback'),
-      onClose: () => {
-        // Remove drawer state from URL
-        navigate(
-          {
-            pathname: baseUrl,
-            query: {
-              ...location.query,
-              cursor: undefined,
-            },
-          },
-          {replace: true}
-        );
-      },
-    });
-  }, [openDrawer, group, project, baseUrl, navigate, location.query]);
-
-  return {openUserFeedbackDrawer};
-}

+ 21 - 0
static/app/views/issueDetails/useGroupUserFeedback.tsx

@@ -0,0 +1,21 @@
+import type {UserReport} from 'sentry/types/group';
+import {useApiQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
+
+interface UseGroupUserFeedbackProps {
+  groupId: string;
+  query: {
+    cursor?: string | undefined;
+  };
+}
+
+export function useGroupUserFeedback({groupId, query}: UseGroupUserFeedbackProps) {
+  const organization = useOrganization();
+
+  return useApiQuery<UserReport[]>(
+    [`/organizations/${organization.slug}/issues/${groupId}/user-reports/`, {query}],
+    {
+      staleTime: 0,
+    }
+  );
+}

+ 1 - 14
static/app/views/issueDetails/utils.tsx

@@ -2,7 +2,7 @@ import {useMemo} from 'react';
 import orderBy from 'lodash/orderBy';
 
 import {bulkUpdate} from 'sentry/actionCreators/group';
-import {Client} from 'sentry/api';
+import type {Client} from 'sentry/api';
 import {t} from 'sentry/locale';
 import ConfigStore from 'sentry/stores/configStore';
 import GroupStore from 'sentry/stores/groupStore';
@@ -34,19 +34,6 @@ export function markEventSeen(
   );
 }
 
-export function fetchGroupUserReports(
-  orgSlug: string,
-  groupId: string,
-  query: Record<string, string>
-) {
-  const api = new Client();
-
-  return api.requestPromise(`/organizations/${orgSlug}/issues/${groupId}/user-reports/`, {
-    includeAllArgs: true,
-    query,
-  });
-}
-
 export function useDefaultIssueEvent() {
   const user = useLegacyStore(ConfigStore).user;
   const options = user ? user.options : null;