accountNotificationFineTuning.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import React from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Pagination from 'app/components/pagination';
  5. import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
  6. import {fields} from 'app/data/forms/accountNotificationSettings';
  7. import {t} from 'app/locale';
  8. import {Organization, Project, UserEmail} from 'app/types';
  9. import withOrganizations from 'app/utils/withOrganizations';
  10. import AsyncView from 'app/views/asyncView';
  11. import {
  12. ACCOUNT_NOTIFICATION_FIELDS,
  13. FineTuneField,
  14. } from 'app/views/settings/account/notifications/fields';
  15. import EmptyMessage from 'app/views/settings/components/emptyMessage';
  16. import Form from 'app/views/settings/components/forms/form';
  17. import JsonForm from 'app/views/settings/components/forms/jsonForm';
  18. import SelectField from 'app/views/settings/components/forms/selectField';
  19. import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
  20. import TextBlock from 'app/views/settings/components/text/textBlock';
  21. const PanelBodyLineItem = styled(PanelBody)`
  22. font-size: 1.4rem;
  23. &:not(:last-child) {
  24. border-bottom: 1px solid ${p => p.theme.innerBorder};
  25. }
  26. `;
  27. // Which fine tuning parts are grouped by project
  28. const isGroupedByProject = (type: string) =>
  29. ['alerts', 'workflow', 'email'].indexOf(type) > -1;
  30. function groupByOrganization(projects: Project[]) {
  31. return projects.reduce<
  32. Record<string, {organization: Organization; projects: Project[]}>
  33. >((acc, project) => {
  34. const orgSlug = project.organization.slug;
  35. if (acc.hasOwnProperty(orgSlug)) {
  36. acc[orgSlug].projects.push(project);
  37. } else {
  38. acc[orgSlug] = {
  39. organization: project.organization,
  40. projects: [project],
  41. };
  42. }
  43. return acc;
  44. }, {});
  45. }
  46. type ANBPProps = {
  47. projects: Project[];
  48. field: FineTuneField;
  49. };
  50. const AccountNotificationsByProject = ({projects, field}: ANBPProps) => {
  51. const projectsByOrg = groupByOrganization(projects);
  52. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  53. const {title, description, ...fieldConfig} = field;
  54. // Display as select box in this view regardless of the type specified in the config
  55. const data = Object.values(projectsByOrg).map(org => ({
  56. name: org.organization.name,
  57. projects: org.projects.map(project => ({
  58. ...fieldConfig,
  59. // `name` key refers to field name
  60. // we use project.id because slugs are not unique across orgs
  61. name: project.id,
  62. label: project.slug,
  63. })),
  64. }));
  65. return (
  66. <React.Fragment>
  67. {data.map(({name, projects: projectFields}) => (
  68. <div key={name}>
  69. <PanelHeader>{name}</PanelHeader>
  70. {projectFields.map(f => (
  71. <PanelBodyLineItem key={f.name}>
  72. <SelectField
  73. defaultValue={f.defaultValue}
  74. name={f.name}
  75. choices={f.choices}
  76. label={f.label}
  77. />
  78. </PanelBodyLineItem>
  79. ))}
  80. </div>
  81. ))}
  82. </React.Fragment>
  83. );
  84. };
  85. type ANBOProps = {
  86. organizations: Organization[];
  87. field: FineTuneField;
  88. };
  89. const AccountNotificationsByOrganization = ({organizations, field}: ANBOProps) => {
  90. // eslint-disable-next-line @typescript-eslint/no-unused-vars
  91. const {title, description, ...fieldConfig} = field;
  92. // Display as select box in this view regardless of the type specified in the config
  93. const data = organizations.map(org => ({
  94. ...fieldConfig,
  95. // `name` key refers to field name
  96. // we use org.id to remain consistent project.id use (which is required because slugs are not unique across orgs)
  97. name: org.id,
  98. label: org.slug,
  99. }));
  100. return (
  101. <React.Fragment>
  102. {data.map(f => (
  103. <PanelBodyLineItem key={f.name}>
  104. <SelectField
  105. defaultValue={f.defaultValue}
  106. name={f.name}
  107. choices={f.choices}
  108. label={f.label}
  109. />
  110. </PanelBodyLineItem>
  111. ))}
  112. </React.Fragment>
  113. );
  114. };
  115. const AccountNotificationsByOrganizationContainer = withOrganizations(
  116. AccountNotificationsByOrganization
  117. );
  118. type Props = AsyncView['props'] & RouteComponentProps<{fineTuneType: string}, {}>;
  119. type State = AsyncView['state'] & {
  120. emails: UserEmail[] | null;
  121. projects: Project[] | null;
  122. notifications: Record<string, any> | null;
  123. fineTuneData: Record<string, any> | null;
  124. };
  125. export default class AccountNotificationFineTuning extends AsyncView<Props, State> {
  126. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  127. const {fineTuneType} = this.props.params;
  128. const endpoints = [
  129. ['notifications', '/users/me/notifications/'],
  130. ['fineTuneData', `/users/me/notifications/${fineTuneType}/`],
  131. ];
  132. if (isGroupedByProject(fineTuneType)) {
  133. endpoints.push(['projects', '/projects/']);
  134. }
  135. endpoints.push(['emails', '/users/me/emails/']);
  136. if (fineTuneType === 'email') {
  137. endpoints.push(['emails', '/users/me/emails/']);
  138. }
  139. return endpoints as ReturnType<AsyncView['getEndpoints']>;
  140. }
  141. // Return a sorted list of user's verified emails
  142. get emailChoices() {
  143. return (
  144. this.state.emails
  145. ?.filter(({isVerified}) => isVerified)
  146. ?.sort((a, b) => {
  147. // Sort by primary -> email
  148. if (a.isPrimary) {
  149. return -1;
  150. } else if (b.isPrimary) {
  151. return 1;
  152. }
  153. return a.email < b.email ? -1 : 1;
  154. }) ?? []
  155. );
  156. }
  157. renderBody() {
  158. const {fineTuneType} = this.props.params;
  159. const {notifications, projects, fineTuneData, projectsPageLinks} = this.state;
  160. const isProject = isGroupedByProject(fineTuneType);
  161. const field = ACCOUNT_NOTIFICATION_FIELDS[fineTuneType];
  162. const {title, description} = field;
  163. const [stateKey, url] = isProject ? this.getEndpoints()[2] : [];
  164. const hasProjects = !!projects?.length;
  165. if (fineTuneType === 'email') {
  166. // Fetch verified email addresses
  167. field.choices = this.emailChoices.map(({email}) => [email, email]);
  168. }
  169. if (!notifications || !fineTuneData) {
  170. return null;
  171. }
  172. return (
  173. <div>
  174. <SettingsPageHeader title={title} />
  175. {description && <TextBlock>{description}</TextBlock>}
  176. {field &&
  177. field.defaultFieldName &&
  178. // not implemented yet
  179. field.defaultFieldName !== 'weeklyReports' && (
  180. <Form
  181. saveOnBlur
  182. apiMethod="PUT"
  183. apiEndpoint="/users/me/notifications/"
  184. initialData={notifications}
  185. >
  186. <JsonForm
  187. title={`Default ${title}`}
  188. fields={[fields[field.defaultFieldName]]}
  189. />
  190. </Form>
  191. )}
  192. <Panel>
  193. <PanelBody>
  194. <PanelHeader hasButtons={isProject}>
  195. <Heading>{isProject ? t('Projects') : t('Organizations')}</Heading>
  196. <div>
  197. {isProject &&
  198. this.renderSearchInput({
  199. placeholder: t('Search Projects'),
  200. url,
  201. stateKey,
  202. })}
  203. </div>
  204. </PanelHeader>
  205. <Form
  206. saveOnBlur
  207. apiMethod="PUT"
  208. apiEndpoint={`/users/me/notifications/${fineTuneType}/`}
  209. initialData={fineTuneData}
  210. >
  211. {isProject && hasProjects && (
  212. <AccountNotificationsByProject projects={projects!} field={field} />
  213. )}
  214. {isProject && !hasProjects && (
  215. <EmptyMessage>{t('No projects found')}</EmptyMessage>
  216. )}
  217. {!isProject && (
  218. <AccountNotificationsByOrganizationContainer field={field} />
  219. )}
  220. </Form>
  221. </PanelBody>
  222. </Panel>
  223. {projects && <Pagination pageLinks={projectsPageLinks} {...this.props} />}
  224. </div>
  225. );
  226. }
  227. }
  228. const Heading = styled('div')`
  229. flex: 1;
  230. `;