projectTeams.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import React from 'react';
  2. import {WithRouterProps} from 'react-router';
  3. import {css} from '@emotion/core';
  4. import styled from '@emotion/styled';
  5. import {addErrorMessage} from 'app/actionCreators/indicator';
  6. import {openCreateTeamModal} from 'app/actionCreators/modal';
  7. import {addTeamToProject, removeTeamFromProject} from 'app/actionCreators/projects';
  8. import Link from 'app/components/links/link';
  9. import Tooltip from 'app/components/tooltip';
  10. import {t} from 'app/locale';
  11. import space from 'app/styles/space';
  12. import {Organization, Project, Team} from 'app/types';
  13. import routeTitleGen from 'app/utils/routeTitle';
  14. import AsyncView from 'app/views/asyncView';
  15. import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
  16. import TeamSelect from 'app/views/settings/components/teamSelect';
  17. type Props = {
  18. organization: Organization;
  19. project: Project;
  20. } & WithRouterProps<{orgId: string; projectId: string}, {}>;
  21. type State = {
  22. projectTeams: null | Team[];
  23. } & AsyncView['state'];
  24. class ProjectTeams extends AsyncView<Props, State> {
  25. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  26. const {orgId, projectId} = this.props.params;
  27. return [['projectTeams', `/projects/${orgId}/${projectId}/teams/`]];
  28. }
  29. getTitle() {
  30. const {projectId} = this.props.params;
  31. return routeTitleGen(t('Project Teams'), projectId, false);
  32. }
  33. canCreateTeam = () => {
  34. const {organization} = this.props;
  35. const access = new Set(organization.access);
  36. return (
  37. access.has('org:write') && access.has('team:write') && access.has('project:write')
  38. );
  39. };
  40. handleRemove = (teamSlug: Team['slug']) => {
  41. if (this.state.loading) {
  42. return;
  43. }
  44. const {orgId, projectId} = this.props.params;
  45. removeTeamFromProject(this.api, orgId, projectId, teamSlug)
  46. .then(() => this.handleRemovedTeam(teamSlug))
  47. .catch(() => {
  48. addErrorMessage(t('Could not remove the %s team', teamSlug));
  49. this.setState({loading: false});
  50. });
  51. };
  52. handleRemovedTeam = (teamSlug: Team['slug']) => {
  53. this.setState(prevState => ({
  54. projectTeams: [
  55. ...(prevState.projectTeams || []).filter(team => team.slug !== teamSlug),
  56. ],
  57. }));
  58. };
  59. handleAddedTeam = (team: Team) => {
  60. this.setState(prevState => ({
  61. projectTeams: [...(prevState.projectTeams || []), team],
  62. }));
  63. };
  64. handleAdd = (team: Team) => {
  65. if (this.state.loading) {
  66. return;
  67. }
  68. const {orgId, projectId} = this.props.params;
  69. addTeamToProject(this.api, orgId, projectId, team).then(
  70. () => {
  71. this.handleAddedTeam(team);
  72. },
  73. () => {
  74. this.setState({
  75. error: true,
  76. loading: false,
  77. });
  78. }
  79. );
  80. };
  81. handleCreateTeam = (e: React.MouseEvent) => {
  82. const {project, organization} = this.props;
  83. if (!this.canCreateTeam()) {
  84. return;
  85. }
  86. e.stopPropagation();
  87. e.preventDefault();
  88. openCreateTeamModal({
  89. project,
  90. organization,
  91. onClose: data => {
  92. addTeamToProject(this.api, organization.slug, project.slug, data).then(
  93. this.remountComponent,
  94. this.remountComponent
  95. );
  96. },
  97. });
  98. };
  99. renderBody() {
  100. const {params, organization} = this.props;
  101. const canCreateTeam = this.canCreateTeam();
  102. const hasAccess = organization.access.includes('project:write');
  103. const confirmRemove = t(
  104. 'This is the last team with access to this project. Removing it will mean ' +
  105. 'only organization owners and managers will be able to access the project pages. Are ' +
  106. 'you sure you want to remove this team from the project %s?',
  107. params.projectId
  108. );
  109. const {projectTeams} = this.state;
  110. const selectedTeams = projectTeams?.map(({slug}) => slug) ?? [];
  111. const menuHeader = (
  112. <StyledTeamsLabel>
  113. {t('Teams')}
  114. <Tooltip
  115. disabled={canCreateTeam}
  116. title={t('You must be a project admin to create teams')}
  117. position="top"
  118. >
  119. <StyledCreateTeamLink
  120. to=""
  121. disabled={!canCreateTeam}
  122. onClick={this.handleCreateTeam}
  123. >
  124. {t('Create Team')}
  125. </StyledCreateTeamLink>
  126. </Tooltip>
  127. </StyledTeamsLabel>
  128. );
  129. return (
  130. <div>
  131. <SettingsPageHeader title={t('%s Teams', params.projectId)} />
  132. <TeamSelect
  133. organization={organization}
  134. selectedTeams={selectedTeams}
  135. onAddTeam={this.handleAdd}
  136. onRemoveTeam={this.handleRemove}
  137. menuHeader={menuHeader}
  138. confirmLastTeamRemoveMessage={confirmRemove}
  139. disabled={!hasAccess}
  140. />
  141. </div>
  142. );
  143. }
  144. }
  145. const StyledTeamsLabel = styled('div')`
  146. font-size: 0.875em;
  147. padding: ${space(0.5)} 0px;
  148. text-transform: uppercase;
  149. `;
  150. const StyledCreateTeamLink = styled(Link)`
  151. float: right;
  152. text-transform: none;
  153. ${p =>
  154. p.disabled &&
  155. css`
  156. cursor: not-allowed;
  157. color: ${p.theme.gray300};
  158. opacity: 0.6;
  159. `};
  160. `;
  161. export default ProjectTeams;