projectsStore.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import {createStore} from 'reflux';
  2. import {fetchOrganizationDetails} from 'sentry/actionCreators/organization';
  3. import {Client} from 'sentry/api';
  4. import type {Team} from 'sentry/types/organization';
  5. import type {Project} from 'sentry/types/project';
  6. import LatestContextStore from './latestContextStore';
  7. import type {StrictStoreDefinition} from './types';
  8. type State = {
  9. loading: boolean;
  10. projects: Project[];
  11. };
  12. type StatsData = Record<string, Project['stats']>;
  13. /**
  14. * Attributes that need typing but aren't part of the external interface,
  15. */
  16. type InternalDefinition = {
  17. api: Client;
  18. removeTeamFromProject(teamSlug: string, project: Project): void;
  19. };
  20. interface ProjectsStoreDefinition
  21. extends InternalDefinition,
  22. StrictStoreDefinition<State> {
  23. getById(id?: string): Project | undefined;
  24. getBySlug(slug?: string): Project | undefined;
  25. isLoading(): boolean;
  26. loadInitialData(projects: Project[]): void;
  27. onAddTeam(team: Team, projectSlug: string): void;
  28. onChangeSlug(prevSlug: string, newSlug: string): void;
  29. onCreateSuccess(project: Project, orgSlug: string): void;
  30. onDeleteTeam(slug: string): void;
  31. onRemoveTeam(teamSlug: string, projectSlug: string): void;
  32. onStatsLoadSuccess(data: StatsData): void;
  33. onUpdateSuccess(data: Partial<Project>): void;
  34. reset(): void;
  35. }
  36. const storeConfig: ProjectsStoreDefinition = {
  37. api: new Client(),
  38. state: {
  39. projects: [],
  40. loading: true,
  41. },
  42. init() {
  43. // XXX: Do not use `this.listenTo` in this store. We avoid usage of reflux
  44. // listeners due to their leaky nature in tests.
  45. this.reset();
  46. },
  47. reset() {
  48. this.state = {
  49. projects: [],
  50. loading: true,
  51. };
  52. },
  53. loadInitialData(items: Project[]) {
  54. this.state = {
  55. projects: items.toSorted((a, b) => a.slug.localeCompare(b.slug)),
  56. loading: false,
  57. };
  58. this.trigger(new Set(items.map(x => x.id)));
  59. },
  60. onChangeSlug(prevSlug: string, newSlug: string) {
  61. const prevProject = this.getBySlug(prevSlug);
  62. if (!prevProject) {
  63. return;
  64. }
  65. const newProject = {...prevProject, slug: newSlug};
  66. const newProjects = this.state.projects
  67. .map(project => (project.slug === prevSlug ? newProject : project))
  68. .toSorted((a, b) => a.slug.localeCompare(b.slug));
  69. this.state = {...this.state, projects: newProjects};
  70. this.trigger(new Set([prevProject.id]));
  71. },
  72. onCreateSuccess(project: Project, orgSlug: string) {
  73. const newProjects = this.state.projects
  74. .concat([project])
  75. .sort((a, b) => a.slug.localeCompare(b.slug));
  76. this.state = {...this.state, projects: newProjects};
  77. // Reload organization details since we've created a new project
  78. fetchOrganizationDetails(this.api, orgSlug, true, false);
  79. this.trigger(new Set([project.id]));
  80. },
  81. onUpdateSuccess(data: Partial<Project>) {
  82. const project = this.getById(data.id);
  83. if (!project) {
  84. return;
  85. }
  86. const newProject = {...project, ...data};
  87. const newProjects = this.state.projects.map(p =>
  88. p.id === project.id ? newProject : p
  89. );
  90. this.state = {...this.state, projects: newProjects};
  91. this.trigger(new Set([data.id]));
  92. LatestContextStore.onUpdateProject(newProject);
  93. },
  94. onStatsLoadSuccess(data) {
  95. const statsData = data || {};
  96. // Assign stats into projects
  97. const newProjects = this.state.projects.map(project =>
  98. statsData[project.id] ? {...project, stats: data[project.id]} : project
  99. );
  100. this.state = {...this.state, projects: newProjects};
  101. this.trigger(new Set(Object.keys(data)));
  102. },
  103. /**
  104. * Listener for when a team is completely removed
  105. *
  106. * @param teamSlug Team Slug
  107. */
  108. onDeleteTeam(teamSlug: string) {
  109. // Look for team in all projects
  110. const projects = this.state.projects.filter(({teams}) =>
  111. teams.find(({slug}) => slug === teamSlug)
  112. );
  113. projects.forEach(project => this.removeTeamFromProject(teamSlug, project));
  114. const affectedProjectIds = projects.map(project => project.id);
  115. this.trigger(new Set(affectedProjectIds));
  116. },
  117. onRemoveTeam(teamSlug: string, projectSlug: string) {
  118. const project = this.getBySlug(projectSlug);
  119. if (!project) {
  120. return;
  121. }
  122. this.removeTeamFromProject(teamSlug, project);
  123. this.trigger(new Set([project.id]));
  124. },
  125. onAddTeam(team: Team, projectSlug: string) {
  126. const project = this.getBySlug(projectSlug);
  127. // Don't do anything if we can't find a project
  128. if (!project) {
  129. return;
  130. }
  131. const newProject = {...project, teams: [...project.teams, team]};
  132. const newProjects = this.state.projects.map(p =>
  133. p.id === project.id ? newProject : p
  134. );
  135. this.state = {...this.state, projects: newProjects};
  136. this.trigger(new Set([project.id]));
  137. },
  138. // Internal method, does not trigger
  139. removeTeamFromProject(teamSlug: string, project: Project) {
  140. const newTeams = project.teams.filter(({slug}) => slug !== teamSlug);
  141. const newProject = {...project, teams: newTeams};
  142. const newProjects = this.state.projects.map(p =>
  143. p.id === project.id ? newProject : p
  144. );
  145. this.state = {...this.state, projects: newProjects};
  146. },
  147. isLoading() {
  148. return this.state.loading;
  149. },
  150. getById(id) {
  151. return this.state.projects.find(project => project.id === id);
  152. },
  153. getBySlug(slug) {
  154. return this.state.projects.find(project => project.slug === slug);
  155. },
  156. getState() {
  157. return this.state;
  158. },
  159. };
  160. const ProjectsStore = createStore(storeConfig);
  161. export default ProjectsStore;