modalManager.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import {Component} from 'react';
  2. import isEqual from 'lodash/isEqual';
  3. import omit from 'lodash/omit';
  4. import {addErrorMessage} from 'sentry/actionCreators/indicator';
  5. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  6. import type {Client} from 'sentry/api';
  7. import {t} from 'sentry/locale';
  8. import type {Organization, Relay} from 'sentry/types';
  9. import createTrustedRelaysResponseError from './createTrustedRelaysResponseError';
  10. import Form from './form';
  11. import Modal from './modal';
  12. type FormProps = React.ComponentProps<typeof Form>;
  13. type Values = FormProps['values'];
  14. type Props = ModalRenderProps & {
  15. api: Client;
  16. onSubmitSuccess: (organization: Organization) => void;
  17. orgSlug: Organization['slug'];
  18. savedRelays: Array<Relay>;
  19. };
  20. type State = {
  21. disables: FormProps['disables'];
  22. errors: FormProps['errors'];
  23. isFormValid: boolean;
  24. requiredValues: Array<keyof Values>;
  25. title: string;
  26. values: Values;
  27. };
  28. class DialogManager<P extends Props = Props, S extends State = State> extends Component<
  29. P,
  30. S
  31. > {
  32. state = this.getDefaultState();
  33. componentDidMount() {
  34. this.validateForm();
  35. }
  36. componentDidUpdate(_prevProps: Props, prevState: S) {
  37. if (!isEqual(prevState.values, this.state.values)) {
  38. this.validateForm();
  39. }
  40. if (
  41. !isEqual(prevState.errors, this.state.errors) &&
  42. Object.keys(this.state.errors).length > 0
  43. ) {
  44. this.setValidForm(false);
  45. }
  46. }
  47. getDefaultState(): Readonly<S> {
  48. return {
  49. values: {name: '', publicKey: '', description: ''},
  50. requiredValues: ['name', 'publicKey'],
  51. errors: {},
  52. disables: {},
  53. isFormValid: false,
  54. title: this.getTitle(),
  55. } as Readonly<S>;
  56. }
  57. getTitle(): string {
  58. return '';
  59. }
  60. getData(): {trustedRelays: Array<Relay>} {
  61. // Child has to implement this
  62. throw new Error('Not implemented');
  63. }
  64. getBtnSaveLabel(): string | undefined {
  65. return undefined;
  66. }
  67. setValidForm(isFormValid: boolean) {
  68. this.setState({isFormValid});
  69. }
  70. validateForm() {
  71. const {values, requiredValues, errors} = this.state;
  72. const isFormValid = requiredValues.every(
  73. requiredValue =>
  74. !!values[requiredValue].replace(/\s/g, '') && !errors[requiredValue]
  75. );
  76. this.setValidForm(isFormValid);
  77. }
  78. clearError<F extends keyof Values>(field: F) {
  79. this.setState(prevState => ({
  80. errors: omit(prevState.errors, field),
  81. }));
  82. }
  83. handleErrorResponse(error: ReturnType<typeof createTrustedRelaysResponseError>) {
  84. switch (error.type) {
  85. case 'invalid-key':
  86. case 'missing-key':
  87. this.setState(prevState => ({
  88. errors: {...prevState.errors, publicKey: error.message},
  89. }));
  90. break;
  91. case 'empty-name':
  92. case 'missing-name':
  93. this.setState(prevState => ({
  94. errors: {...prevState.errors, name: error.message},
  95. }));
  96. break;
  97. default:
  98. addErrorMessage(error.message);
  99. }
  100. }
  101. handleChange = <F extends keyof Values>(field: F, value: Values[F]) => {
  102. this.setState(prevState => ({
  103. values: {
  104. ...prevState.values,
  105. [field]: value,
  106. },
  107. errors: omit(prevState.errors, field),
  108. }));
  109. };
  110. handleSave = async () => {
  111. const {onSubmitSuccess, closeModal, orgSlug, api} = this.props;
  112. const trustedRelays = this.getData().trustedRelays.map(trustedRelay =>
  113. omit(trustedRelay, ['created', 'lastModified'])
  114. );
  115. try {
  116. const response = await api.requestPromise(`/organizations/${orgSlug}/`, {
  117. method: 'PUT',
  118. data: {trustedRelays},
  119. });
  120. onSubmitSuccess(response);
  121. closeModal();
  122. } catch (error) {
  123. this.handleErrorResponse(createTrustedRelaysResponseError(error));
  124. }
  125. };
  126. handleValidate =
  127. <F extends keyof Values>(field: F) =>
  128. () => {
  129. const isFieldValueEmpty = !this.state.values[field].replace(/\s/g, '');
  130. const fieldErrorAlreadyExist = this.state.errors[field];
  131. if (isFieldValueEmpty && fieldErrorAlreadyExist) {
  132. return;
  133. }
  134. if (isFieldValueEmpty && !fieldErrorAlreadyExist) {
  135. this.setState(prevState => ({
  136. errors: {
  137. ...prevState.errors,
  138. [field]: t('Field Required'),
  139. },
  140. }));
  141. return;
  142. }
  143. if (!isFieldValueEmpty && fieldErrorAlreadyExist) {
  144. this.clearError(field);
  145. }
  146. };
  147. handleValidateKey = () => {
  148. const {savedRelays} = this.props;
  149. const {values, errors} = this.state;
  150. const isKeyAlreadyTaken = savedRelays.find(
  151. savedRelay => savedRelay.publicKey === values.publicKey
  152. );
  153. if (isKeyAlreadyTaken && !errors.publicKey) {
  154. this.setState({
  155. errors: {
  156. ...errors,
  157. publicKey: t('Relay key already taken'),
  158. },
  159. });
  160. return;
  161. }
  162. if (errors.publicKey) {
  163. this.setState({
  164. errors: omit(errors, 'publicKey'),
  165. });
  166. }
  167. this.handleValidate('publicKey')();
  168. };
  169. getForm() {
  170. const {values, errors, disables, isFormValid} = this.state;
  171. return (
  172. <Form
  173. isFormValid={isFormValid}
  174. onSave={this.handleSave}
  175. onChange={this.handleChange}
  176. onValidate={this.handleValidate}
  177. onValidateKey={this.handleValidateKey}
  178. errors={errors}
  179. values={values}
  180. disables={disables}
  181. />
  182. );
  183. }
  184. getContent(): React.ReactElement {
  185. return this.getForm();
  186. }
  187. render() {
  188. const {title, isFormValid} = this.state;
  189. const btnSaveLabel = this.getBtnSaveLabel();
  190. const content = this.getContent();
  191. return (
  192. <Modal
  193. {...this.props}
  194. title={title}
  195. onSave={this.handleSave}
  196. btnSaveLabel={btnSaveLabel}
  197. disabled={!isFormValid}
  198. content={content}
  199. />
  200. );
  201. }
  202. }
  203. export default DialogManager;