jsonForm.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import * as React from 'react';
  2. import {withRouter, WithRouterProps} from 'react-router';
  3. import * as Sentry from '@sentry/react';
  4. import scrollToElement from 'scroll-to-element';
  5. import {defined} from 'sentry/utils';
  6. import {sanitizeQuerySelector} from 'sentry/utils/sanitizeQuerySelector';
  7. import FormPanel from './formPanel';
  8. import {Field, FieldObject, JsonFormObject} from './type';
  9. type Props = {
  10. additionalFieldProps?: {[key: string]: any};
  11. /**
  12. * If `forms` is not defined, `title` + `fields` must be required.
  13. * Allows more fine grain control of title/fields
  14. */
  15. fields?: FieldObject[];
  16. /**
  17. * Fields that are grouped by "section"
  18. */
  19. forms?: JsonFormObject[];
  20. } & WithRouterProps &
  21. Omit<
  22. React.ComponentProps<typeof FormPanel>,
  23. 'highlighted' | 'fields' | 'additionalFieldProps'
  24. >;
  25. type State = {
  26. // Field name that should be highlighted
  27. highlighted?: string;
  28. };
  29. class JsonForm extends React.Component<Props, State> {
  30. state: State = {
  31. // location.hash is optional because of tests.
  32. highlighted: this.props.location?.hash,
  33. };
  34. componentDidMount() {
  35. this.scrollToHash();
  36. }
  37. UNSAFE_componentWillReceiveProps(nextProps: Props) {
  38. if (nextProps.location && this.props.location.hash !== nextProps.location.hash) {
  39. const hash = nextProps.location.hash;
  40. this.scrollToHash(hash);
  41. this.setState({highlighted: hash});
  42. }
  43. }
  44. scrollToHash(toHash?: string): void {
  45. // location.hash is optional because of tests.
  46. const hash = toHash || this.props.location?.hash;
  47. if (!hash) {
  48. return;
  49. }
  50. // Push onto callback queue so it runs after the DOM is updated,
  51. // this is required when navigating from a different page so that
  52. // the element is rendered on the page before trying to getElementById.
  53. try {
  54. scrollToElement(sanitizeQuerySelector(decodeURIComponent(hash)), {
  55. align: 'middle',
  56. offset: -100,
  57. });
  58. } catch (err) {
  59. Sentry.captureException(err);
  60. }
  61. }
  62. shouldDisplayForm(fields: FieldObject[]) {
  63. const fieldsWithVisibleProp = fields.filter(
  64. field => typeof field !== 'function' && defined(field?.visible)
  65. ) as Array<Omit<Field, 'visible'> & Required<Pick<Field, 'visible'>>>;
  66. if (fields.length === fieldsWithVisibleProp.length) {
  67. const {additionalFieldProps, ...props} = this.props;
  68. const areAllFieldsHidden = fieldsWithVisibleProp.every(field => {
  69. if (typeof field.visible === 'function') {
  70. return !field.visible({...props, ...additionalFieldProps});
  71. }
  72. return !field.visible;
  73. });
  74. return !areAllFieldsHidden;
  75. }
  76. return true;
  77. }
  78. renderForm({
  79. fields,
  80. formPanelProps,
  81. title,
  82. }: {
  83. fields: FieldObject[];
  84. formPanelProps: Pick<
  85. Props,
  86. | 'access'
  87. | 'disabled'
  88. | 'features'
  89. | 'additionalFieldProps'
  90. | 'renderFooter'
  91. | 'renderHeader'
  92. > &
  93. Pick<State, 'highlighted'>;
  94. title?: React.ReactNode;
  95. }) {
  96. const shouldDisplayForm = this.shouldDisplayForm(fields);
  97. if (
  98. !shouldDisplayForm &&
  99. !formPanelProps?.renderFooter &&
  100. !formPanelProps?.renderHeader
  101. ) {
  102. return null;
  103. }
  104. return <FormPanel title={title} fields={fields} {...formPanelProps} />;
  105. }
  106. render() {
  107. const {
  108. access,
  109. collapsible,
  110. fields,
  111. title,
  112. forms,
  113. disabled,
  114. features,
  115. additionalFieldProps,
  116. renderFooter,
  117. renderHeader,
  118. location: _location,
  119. ...otherProps
  120. } = this.props;
  121. const formPanelProps = {
  122. access,
  123. disabled,
  124. features,
  125. additionalFieldProps,
  126. renderFooter,
  127. renderHeader,
  128. highlighted: this.state.highlighted,
  129. collapsible,
  130. };
  131. return (
  132. <div {...otherProps}>
  133. {typeof forms !== 'undefined' &&
  134. forms.map((formGroup, i) => (
  135. <React.Fragment key={i}>
  136. {this.renderForm({formPanelProps, ...formGroup})}
  137. </React.Fragment>
  138. ))}
  139. {typeof forms === 'undefined' &&
  140. typeof fields !== 'undefined' &&
  141. this.renderForm({fields, formPanelProps, title})}
  142. </div>
  143. );
  144. }
  145. }
  146. export default withRouter(JsonForm);