Browse Source

ref(ui) Convert another batch of components to FCs (#28658)

Mark Story 3 years ago
parent
commit
a64d4f8c13

+ 14 - 13
static/app/components/lightWeightNoProjectMessage.tsx

@@ -1,5 +1,3 @@
-import {Component} from 'react';
-
 import NoProjectMessage from 'app/components/noProjectMessage';
 import {LightWeightOrganization, Organization, Project} from 'app/types';
 import withProjects from 'app/utils/withProjects';
@@ -10,17 +8,20 @@ type Props = {
   loadingProjects: boolean;
 };
 
-class LightWeightNoProjectMessage extends Component<Props> {
-  render() {
-    const {organization, projects, loadingProjects} = this.props;
-    return (
-      <NoProjectMessage
-        {...this.props}
-        projects={projects}
-        loadingProjects={!('projects' in organization) && loadingProjects}
-      />
-    );
-  }
+function LightWeightNoProjectMessage({
+  organization,
+  projects,
+  loadingProjects,
+  ...props
+}: Props) {
+  return (
+    <NoProjectMessage
+      {...props}
+      organization={organization}
+      projects={projects}
+      loadingProjects={!('projects' in organization) && loadingProjects}
+    />
+  );
 }
 
 export default withProjects(LightWeightNoProjectMessage);

+ 6 - 8
static/app/components/mutedBox.tsx

@@ -1,5 +1,3 @@
-import {PureComponent} from 'react';
-
 import DateTime from 'app/components/dateTime';
 import Duration from 'app/components/duration';
 import {BannerContainer, BannerSummary} from 'app/components/events/styles';
@@ -11,10 +9,10 @@ type Props = {
   statusDetails: ResolutionStatusDetails;
 };
 
-class MutedBox extends PureComponent<Props> {
-  renderReason = () => {
+function MutedBox({statusDetails}: Props) {
+  function renderReason() {
     const {ignoreUntil, ignoreCount, ignoreWindow, ignoreUserCount, ignoreUserWindow} =
-      this.props.statusDetails;
+      statusDetails;
 
     if (ignoreUntil) {
       return t(
@@ -52,14 +50,14 @@ class MutedBox extends PureComponent<Props> {
     }
 
     return t('This issue has been ignored');
-  };
+  }
 
-  render = () => (
+  return (
     <BannerContainer priority="default">
       <BannerSummary>
         <IconMute color="red300" size="sm" />
         <span>
-          {this.renderReason()}&nbsp;&mdash;&nbsp;
+          {renderReason()}&nbsp;&mdash;&nbsp;
           {t(
             'You will not be notified of any changes and it will not show up by default in feeds.'
           )}

+ 85 - 90
static/app/components/noProjectMessage.tsx

@@ -1,4 +1,4 @@
-import {Component, Fragment} from 'react';
+import {Fragment} from 'react';
 import styled from '@emotion/styled';
 
 /* TODO: replace with I/O when finished */
@@ -12,100 +12,95 @@ import ConfigStore from 'app/stores/configStore';
 import space from 'app/styles/space';
 import {LightWeightOrganization, Organization, Project} from 'app/types';
 
-type Props = {
+type Props = React.PropsWithChildren<{
   organization: LightWeightOrganization | Organization;
   projects?: Project[];
   loadingProjects?: boolean;
   superuserNeedsToBeProjectMember?: boolean;
-};
-
-export default class NoProjectMessage extends Component<Props> {
-  render() {
-    const {
-      children,
-      organization,
-      projects,
-      loadingProjects,
-      superuserNeedsToBeProjectMember,
-    } = this.props;
-    const orgId = organization.slug;
-    const canCreateProject = organization.access.includes('project:write');
-    const canJoinTeam = organization.access.includes('team:read');
-
-    let orgHasProjects: boolean;
-    let hasProjectAccess: boolean;
-
-    if ('projects' in organization) {
-      const {isSuperuser} = ConfigStore.get('user');
-
-      orgHasProjects = organization.projects.length > 0;
-      hasProjectAccess =
-        isSuperuser && !superuserNeedsToBeProjectMember
-          ? organization.projects.some(p => p.hasAccess)
-          : organization.projects.some(p => p.isMember && p.hasAccess);
-    } else {
-      hasProjectAccess = projects ? projects.length > 0 : false;
-      orgHasProjects = hasProjectAccess;
-    }
-
-    if (hasProjectAccess || loadingProjects) {
-      return children;
-    }
-
-    // If the organization has some projects, but the user doesn't have access to
-    // those projects, the primary action is to Join a Team. Otherwise the primary
-    // action is to create a project.
-
-    const joinTeamAction = (
-      <Button
-        title={canJoinTeam ? undefined : t('You do not have permission to join a team.')}
-        disabled={!canJoinTeam}
-        priority={orgHasProjects ? 'primary' : 'default'}
-        to={`/settings/${orgId}/teams/`}
-      >
-        {t('Join a Team')}
-      </Button>
-    );
-
-    const createProjectAction = (
-      <Button
-        title={
-          canCreateProject
-            ? undefined
-            : t('You do not have permission to create a project.')
-        }
-        disabled={!canCreateProject}
-        priority={orgHasProjects ? 'default' : 'primary'}
-        to={`/organizations/${orgId}/projects/new/`}
-      >
-        {t('Create project')}
-      </Button>
-    );
-
-    return (
-      <Wrapper>
-        <HeightWrapper>
-          <img src={img} height={350} alt="Nothing to see" />
-          <Content>
-            <StyledPageHeading>{t('Remain Calm')}</StyledPageHeading>
-            <HelpMessage>
-              {t('You need at least one project to use this view')}
-            </HelpMessage>
-            <Actions gap={1}>
-              {!orgHasProjects ? (
-                createProjectAction
-              ) : (
-                <Fragment>
-                  {joinTeamAction}
-                  {createProjectAction}
-                </Fragment>
-              )}
-            </Actions>
-          </Content>
-        </HeightWrapper>
-      </Wrapper>
-    );
+}>;
+
+export default function NoProjectMessage({
+  children,
+  organization,
+  projects,
+  loadingProjects,
+  superuserNeedsToBeProjectMember,
+}: Props) {
+  const orgSlug = organization.slug;
+  const canCreateProject = organization.access.includes('project:write');
+  const canJoinTeam = organization.access.includes('team:read');
+
+  let orgHasProjects = false;
+  let hasProjectAccess = false;
+
+  if ('projects' in organization) {
+    const {isSuperuser} = ConfigStore.get('user');
+
+    orgHasProjects = organization.projects.length > 0;
+    hasProjectAccess =
+      isSuperuser && !superuserNeedsToBeProjectMember
+        ? organization.projects.some(p => p.hasAccess)
+        : organization.projects.some(p => p.isMember && p.hasAccess);
+  } else {
+    hasProjectAccess = projects ? projects.length > 0 : false;
+    orgHasProjects = hasProjectAccess;
   }
+
+  if (hasProjectAccess || loadingProjects) {
+    return <Fragment>{children}</Fragment>;
+  }
+
+  // If the organization has some projects, but the user doesn't have access to
+  // those projects, the primary action is to Join a Team. Otherwise the primary
+  // action is to create a project.
+
+  const joinTeamAction = (
+    <Button
+      title={canJoinTeam ? undefined : t('You do not have permission to join a team.')}
+      disabled={!canJoinTeam}
+      priority={orgHasProjects ? 'primary' : 'default'}
+      to={`/settings/${orgSlug}/teams/`}
+    >
+      {t('Join a Team')}
+    </Button>
+  );
+
+  const createProjectAction = (
+    <Button
+      title={
+        canCreateProject
+          ? undefined
+          : t('You do not have permission to create a project.')
+      }
+      disabled={!canCreateProject}
+      priority={orgHasProjects ? 'default' : 'primary'}
+      to={`/organizations/${orgSlug}/projects/new/`}
+    >
+      {t('Create project')}
+    </Button>
+  );
+
+  return (
+    <Wrapper>
+      <HeightWrapper>
+        <img src={img} height={350} alt={t('Nothing to see')} />
+        <Content>
+          <StyledPageHeading>{t('Remain Calm')}</StyledPageHeading>
+          <HelpMessage>{t('You need at least one project to use this view')}</HelpMessage>
+          <Actions gap={1}>
+            {!orgHasProjects ? (
+              createProjectAction
+            ) : (
+              <Fragment>
+                {joinTeamAction}
+                {createProjectAction}
+              </Fragment>
+            )}
+          </Actions>
+        </Content>
+      </HeightWrapper>
+    </Wrapper>
+  );
 }
 
 const StyledPageHeading = styled(PageHeading)`

+ 45 - 48
static/app/components/organizations/globalSelectionHeader/index.tsx

@@ -24,9 +24,21 @@ type Props = {
     Pick<React.ComponentProps<typeof InitializeGlobalSelectionHeader>, 'skipLoadLastUsed'>
   >;
 
-class GlobalSelectionHeaderContainer extends React.Component<Props> {
-  getProjects = () => {
-    const {organization, projects} = this.props;
+function GlobalSelectionHeaderContainer({
+  organization,
+  projects,
+  loadingProjects,
+  location,
+  router,
+  routes,
+  defaultSelection,
+  forceProject,
+  shouldForceProject,
+  skipLoadLastUsed,
+  showAbsolute,
+  ...props
+}: Props) {
+  function getProjects() {
     const {isSuperuser} = ConfigStore.get('user');
     const isOrgAdmin = organization.access.includes('org:admin');
 
@@ -40,60 +52,45 @@ class GlobalSelectionHeaderContainer extends React.Component<Props> {
     }
 
     return [memberProjects, []];
-  };
-
-  render() {
-    const {
-      loadingProjects,
-      location,
-      organization,
-      router,
-      routes,
+  }
 
-      defaultSelection,
-      forceProject,
-      shouldForceProject,
-      skipLoadLastUsed,
-      showAbsolute,
-      ...props
-    } = this.props;
-    const enforceSingleProject = !organization.features.includes('global-views');
-    const [memberProjects, nonMemberProjects] = this.getProjects();
+  const enforceSingleProject = !organization.features.includes('global-views');
+  const [memberProjects, nonMemberProjects] = getProjects();
 
-    // We can initialize before ProjectsStore is fully loaded if we don't need to enforce single project.
-    return (
-      <React.Fragment>
-        {(!loadingProjects || (!shouldForceProject && !enforceSingleProject)) && (
-          <InitializeGlobalSelectionHeader
-            location={location}
-            skipLoadLastUsed={!!skipLoadLastUsed}
-            router={router}
-            organization={organization}
-            defaultSelection={defaultSelection}
-            forceProject={forceProject}
-            shouldForceProject={!!shouldForceProject}
-            shouldEnforceSingleProject={enforceSingleProject}
-            memberProjects={memberProjects}
-            showAbsolute={showAbsolute}
-          />
-        )}
-        <GlobalSelectionHeader
-          {...props}
-          loadingProjects={loadingProjects}
+  // We can initialize before ProjectsStore is fully loaded if we don't need to enforce single project.
+  return (
+    <React.Fragment>
+      {(!loadingProjects || (!shouldForceProject && !enforceSingleProject)) && (
+        <InitializeGlobalSelectionHeader
           location={location}
-          organization={organization}
+          skipLoadLastUsed={!!skipLoadLastUsed}
           router={router}
-          routes={routes}
-          shouldForceProject={!!shouldForceProject}
+          organization={organization}
           defaultSelection={defaultSelection}
           forceProject={forceProject}
+          shouldForceProject={!!shouldForceProject}
+          shouldEnforceSingleProject={enforceSingleProject}
           memberProjects={memberProjects}
-          nonMemberProjects={nonMemberProjects}
           showAbsolute={showAbsolute}
         />
-      </React.Fragment>
-    );
-  }
+      )}
+      <GlobalSelectionHeader
+        {...props}
+        loadingProjects={loadingProjects}
+        location={location}
+        organization={organization}
+        router={router}
+        routes={routes}
+        projects={projects}
+        shouldForceProject={!!shouldForceProject}
+        defaultSelection={defaultSelection}
+        forceProject={forceProject}
+        memberProjects={memberProjects}
+        nonMemberProjects={nonMemberProjects}
+        showAbsolute={showAbsolute}
+      />
+    </React.Fragment>
+  );
 }
 
 export default withOrganization(

+ 59 - 73
static/app/components/organizations/headerItem.tsx

@@ -29,85 +29,71 @@ type Props = {
 } & Partial<DefaultProps> &
   React.HTMLAttributes<HTMLDivElement>;
 
-class HeaderItem extends React.Component<Props> {
-  static defaultProps: DefaultProps = {
-    allowClear: true,
+function HeaderItem({
+  children,
+  isOpen,
+  hasSelected,
+  icon,
+  locked,
+  lockedMessage,
+  settingsLink,
+  hint,
+  loading,
+  forwardRef,
+  onClear,
+  allowClear = true,
+  ...props
+}: Props) {
+  const handleClear = (e: React.MouseEvent) => {
+    e.stopPropagation();
+    onClear?.();
   };
 
-  handleClear = (e: React.MouseEvent) => {
-    e.stopPropagation();
-    this.props.onClear?.();
+  const textColorProps = {
+    locked,
+    isOpen,
+    hasSelected,
   };
 
-  render() {
-    const {
-      children,
-      isOpen,
-      hasSelected,
-      allowClear,
-      icon,
-      locked,
-      lockedMessage,
-      settingsLink,
-      hint,
-      loading,
-      forwardRef,
-      ...props
-    } = this.props;
-
-    const textColorProps = {
-      locked,
-      isOpen,
-      hasSelected,
-    };
-
-    return (
-      <StyledHeaderItem
-        ref={forwardRef}
-        loading={!!loading}
-        {...omit(props, 'onClear')}
-        {...textColorProps}
-      >
-        <IconContainer {...textColorProps}>{icon}</IconContainer>
-        <Content>
-          <StyledContent>{children}</StyledContent>
-
-          {settingsLink && (
-            <SettingsIconLink to={settingsLink}>
-              <IconSettings />
-            </SettingsIconLink>
-          )}
-        </Content>
-        {hint && (
-          <Hint>
-            <Tooltip title={hint} position="bottom">
-              <IconInfo size="sm" />
-            </Tooltip>
-          </Hint>
-        )}
-        {hasSelected && !locked && allowClear && (
-          <StyledClose {...textColorProps} onClick={this.handleClear} />
+  return (
+    <StyledHeaderItem
+      ref={forwardRef}
+      loading={!!loading}
+      {...omit(props, 'onClear')}
+      {...textColorProps}
+    >
+      <IconContainer {...textColorProps}>{icon}</IconContainer>
+      <Content>
+        <StyledContent>{children}</StyledContent>
+
+        {settingsLink && (
+          <SettingsIconLink to={settingsLink}>
+            <IconSettings />
+          </SettingsIconLink>
         )}
-        {!locked && !loading && (
-          <ChevronWrapper>
-            <StyledChevron
-              isOpen={!!isOpen}
-              direction={isOpen ? 'up' : 'down'}
-              size="sm"
-            />
-          </ChevronWrapper>
-        )}
-        {locked && (
-          <Tooltip
-            title={lockedMessage || t('This selection is locked')}
-            position="bottom"
-          >
-            <StyledLock color="gray300" />
+      </Content>
+      {hint && (
+        <Hint>
+          <Tooltip title={hint} position="bottom">
+            <IconInfo size="sm" />
           </Tooltip>
-        )}
-      </StyledHeaderItem>
-    );
-  }
+        </Hint>
+      )}
+      {hasSelected && !locked && allowClear && (
+        <StyledClose {...textColorProps} onClick={handleClear} />
+      )}
+      {!locked && !loading && (
+        <ChevronWrapper>
+          <StyledChevron isOpen={!!isOpen} direction={isOpen ? 'up' : 'down'} size="sm" />
+        </ChevronWrapper>
+      )}
+      {locked && (
+        <Tooltip title={lockedMessage || t('This selection is locked')} position="bottom">
+          <StyledLock color="gray300" />
+        </Tooltip>
+      )}
+    </StyledHeaderItem>
+  );
 }
 
 // Infer props here because of styled/theme

+ 64 - 63
static/app/components/sidebar/onboardingStatus.tsx

@@ -1,4 +1,4 @@
-import {Component, Fragment} from 'react';
+import {Fragment} from 'react';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 
@@ -29,10 +29,15 @@ const progressTextCss = () => css`
   font-weight: bold;
 `;
 
-class OnboardingStatus extends Component<Props> {
-  handleShowPanel = () => {
-    const {org, onShowPanel} = this.props;
-
+function OnboardingStatus({
+  collapsed,
+  org,
+  currentPanel,
+  orientation,
+  hidePanel,
+  onShowPanel,
+}: Props) {
+  const handleShowPanel = () => {
     trackAnalyticsEvent({
       eventKey: 'onboarding.wizard_opened',
       eventName: 'Onboarding Wizard Opened',
@@ -41,65 +46,61 @@ class OnboardingStatus extends Component<Props> {
     onShowPanel();
   };
 
-  render() {
-    const {collapsed, org, currentPanel, orientation, hidePanel} = this.props;
-
-    if (!(org.features && org.features.includes('onboarding'))) {
-      return null;
-    }
-
-    const tasks = getMergedTasks(org);
-
-    const allDisplayedTasks = tasks.filter(task => task.display);
-    const doneTasks = allDisplayedTasks.filter(isDone);
-    const numberRemaining = allDisplayedTasks.length - doneTasks.length;
-
-    const pendingCompletionSeen = doneTasks.some(
-      task =>
-        allDisplayedTasks.some(displayedTask => displayedTask.task === task.task) &&
-        task.status === 'complete' &&
-        !task.completionSeen
-    );
-
-    const isActive = currentPanel === SidebarPanelKey.OnboardingWizard;
-
-    if (doneTasks.length >= allDisplayedTasks.length && !isActive) {
-      return null;
-    }
-
-    return (
-      <Fragment>
-        <Container onClick={this.handleShowPanel} isActive={isActive}>
-          <ProgressRing
-            animateText
-            textCss={progressTextCss}
-            text={allDisplayedTasks.length - doneTasks.length}
-            value={(doneTasks.length / allDisplayedTasks.length) * 100}
-            backgroundColor="rgba(255, 255, 255, 0.15)"
-            progressEndcaps="round"
-            size={38}
-            barWidth={6}
-          />
-          {!collapsed && (
-            <div>
-              <Heading>{t('Quick Start')}</Heading>
-              <Remaining>
-                {tct('[numberRemaining] Remaining tasks', {numberRemaining})}
-                {pendingCompletionSeen && <PendingSeenIndicator />}
-              </Remaining>
-            </div>
-          )}
-        </Container>
-        {isActive && (
-          <OnboardingSidebar
-            orientation={orientation}
-            collapsed={collapsed}
-            onClose={hidePanel}
-          />
-        )}
-      </Fragment>
-    );
+  if (!org.features?.includes('onboarding')) {
+    return null;
+  }
+
+  const tasks = getMergedTasks(org);
+
+  const allDisplayedTasks = tasks.filter(task => task.display);
+  const doneTasks = allDisplayedTasks.filter(isDone);
+  const numberRemaining = allDisplayedTasks.length - doneTasks.length;
+
+  const pendingCompletionSeen = doneTasks.some(
+    task =>
+      allDisplayedTasks.some(displayedTask => displayedTask.task === task.task) &&
+      task.status === 'complete' &&
+      !task.completionSeen
+  );
+
+  const isActive = currentPanel === SidebarPanelKey.OnboardingWizard;
+
+  if (doneTasks.length >= allDisplayedTasks.length && !isActive) {
+    return null;
   }
+
+  return (
+    <Fragment>
+      <Container onClick={handleShowPanel} isActive={isActive}>
+        <ProgressRing
+          animateText
+          textCss={progressTextCss}
+          text={allDisplayedTasks.length - doneTasks.length}
+          value={(doneTasks.length / allDisplayedTasks.length) * 100}
+          backgroundColor="rgba(255, 255, 255, 0.15)"
+          progressEndcaps="round"
+          size={38}
+          barWidth={6}
+        />
+        {!collapsed && (
+          <div>
+            <Heading>{t('Quick Start')}</Heading>
+            <Remaining>
+              {tct('[numberRemaining] Remaining tasks', {numberRemaining})}
+              {pendingCompletionSeen && <PendingSeenIndicator />}
+            </Remaining>
+          </div>
+        )}
+      </Container>
+      {isActive && (
+        <OnboardingSidebar
+          orientation={orientation}
+          collapsed={collapsed}
+          onClose={hidePanel}
+        />
+      )}
+    </Fragment>
+  );
 }
 
 const Heading = styled('div')`