Browse Source

ref(ts): Consolidate types for Field into one place (#40699)

Evan Purkhiser 2 years ago
parent
commit
a2aca39986

+ 3 - 2
static/app/components/forms/field/controlState.tsx

@@ -5,11 +5,11 @@ import Spinner from 'sentry/components/forms/spinner';
 import {IconCheckmark, IconWarning} from 'sentry/icons';
 import {fadeOut, pulse} from 'sentry/styles/animations';
 
-export interface ControlStateProps {
+interface ControlStateProps {
   /**
    * Display the  error indicator
    */
-  error?: string | boolean;
+  error?: boolean;
   /**
    * Display the "was just saved" state
    */
@@ -73,4 +73,5 @@ const FieldError = styled('div')`
   color: ${p => p.theme.red300};
   animation: ${() => pulse(1.15)} 1s ease infinite;
 `;
+
 export default ControlState;

+ 14 - 39
static/app/components/forms/field/fieldControl.tsx

@@ -4,46 +4,21 @@ import QuestionTooltip from 'sentry/components/questionTooltip';
 import space from 'sentry/styles/space';
 
 import FieldControlState from './fieldControlState';
+import {FieldGroupProps} from './types';
 
-export interface FieldControlProps {
+type FieldControlProps = Pick<
+  FieldGroupProps,
+  | 'alignRight'
+  | 'controlState'
+  | 'disabled'
+  | 'disabledReason'
+  | 'errorState'
+  | 'flexibleControlStateSize'
+  | 'hideControlState'
+  | 'inline'
+> & {
   children: React.ReactNode;
-  /**
-   * Align the control towards the right
-   */
-  alignRight?: boolean;
-  /**
-   * Loading / Saving / Error states of the form. See the ControlState
-   */
-  controlState?: React.ReactNode;
-  /**
-   * Disable the field
-   */
-  disabled?: boolean;
-  /**
-   * Produces a question tooltip on the field, explaining why it is disabled
-   */
-  disabledReason?: React.ReactNode;
-  /**
-   * The error state. Will not be rendered if hideControlState is true
-   */
-  errorState?: React.ReactNode;
-  /**
-   * Allow the control state to flex based on its content. When enabled the
-   * control state element will NOT take up space unless it has some state to
-   * show (such as an error).
-   */
-  flexibleControlStateSize?: boolean;
-  /**
-   * Hide the fields control state
-   */
-  hideControlState?: boolean;
-  /**
-   * Display the field control container in "inline" fashion. The label and
-   * description will be aligned to the left, while the control itself will be
-   * aligned to the right.
-   */
-  inline?: boolean;
-}
+};
 
 const FieldControl = ({
   inline,
@@ -54,7 +29,7 @@ const FieldControl = ({
   controlState,
   children,
   hideControlState,
-  flexibleControlStateSize = false,
+  flexibleControlStateSize,
 }: FieldControlProps) => (
   <FieldControlErrorWrapper inline={inline}>
     <FieldControlWrapper>

+ 5 - 9
static/app/components/forms/field/fieldControlState.tsx

@@ -1,17 +1,13 @@
 import styled from '@emotion/styled';
 
-const width = '36px';
+import {FieldGroupProps} from './types';
 
-const FieldControlState = styled('div')<{
-  /**
-   * Do not apply a width to the control state container, allowing it to flex
-   * based on its parents constraints.
-   */
-  flexibleControlStateSize?: boolean;
-}>`
+type FieldControlStateProps = Pick<FieldGroupProps, 'flexibleControlStateSize'>;
+
+const FieldControlState = styled('div')<FieldControlStateProps>`
   display: flex;
   position: relative;
-  ${p => !p.flexibleControlStateSize && `width: ${width}`};
+  ${p => !p.flexibleControlStateSize && `width: 36px`};
   flex-shrink: 0;
   justify-content: center;
   align-items: center;

+ 6 - 2
static/app/components/forms/field/fieldDescription.tsx

@@ -3,7 +3,11 @@ import styled from '@emotion/styled';
 
 import space from 'sentry/styles/space';
 
-const inlineStyle = p =>
+import {FieldGroupProps} from './types';
+
+type FieldDescriptionProps = Pick<FieldGroupProps, 'inline'>;
+
+const inlineStyle = (p: FieldDescriptionProps) =>
   p.inline
     ? css`
         width: 50%;
@@ -14,7 +18,7 @@ const inlineStyle = p =>
         margin-bottom: ${space(1)};
       `;
 
-const FieldDescription = styled('label')<{inline?: boolean}>`
+const FieldDescription = styled('label')<FieldDescriptionProps>`
   font-weight: normal;
   margin-bottom: 0;
 

+ 5 - 1
static/app/components/forms/field/fieldHelp.tsx

@@ -2,7 +2,11 @@ import styled from '@emotion/styled';
 
 import space from 'sentry/styles/space';
 
-const FieldHelp = styled('div')<{inline?: boolean; stacked?: boolean}>`
+import {FieldGroupProps} from './types';
+
+type FieldHelpProps = Pick<FieldGroupProps, 'inline' | 'stacked'>;
+
+const FieldHelp = styled('div')<FieldHelpProps>`
   color: ${p => p.theme.subText};
   font-size: ${p => p.theme.fontSizeSmall};
   margin-top: ${p => (p.stacked && !p.inline ? 0 : space(0.5))};

+ 5 - 1
static/app/components/forms/field/fieldLabel.tsx

@@ -3,9 +3,13 @@ import styled from '@emotion/styled';
 
 import space from 'sentry/styles/space';
 
+import {FieldGroupProps} from './types';
+
+type FieldLabelProps = Pick<FieldGroupProps, 'disabled'>;
+
 const shouldForwardProp = p => p !== 'disabled' && isPropValid(p);
 
-const FieldLabel = styled('div', {shouldForwardProp})<{disabled?: boolean}>`
+const FieldLabel = styled('div', {shouldForwardProp})<FieldLabelProps>`
   color: ${p => (!p.disabled ? p.theme.textColor : p.theme.disabled)};
   display: flex;
   gap: ${space(0.5)};

+ 6 - 26
static/app/components/forms/field/fieldWrapper.tsx

@@ -3,32 +3,12 @@ import styled from '@emotion/styled';
 
 import space from 'sentry/styles/space';
 
-/**
- * Using Parameters<typeof FieldWrapper> in the Field component somehow
- * causes an infinite recursive depth so exporting the props is best workaround
- */
-export interface FieldWrapperProps {
-  /**
-   * When false adds padding to the right of the element to ensure visual
-   * consistency with other fields that aren't using flexible control states.
-   */
-  hasControlState?: boolean;
-  /**
-   * Is "highlighted", i.e. after a search
-   */
-  highlighted?: boolean;
-  /**
-   * Display the field control container in "inline" fashion. The label and
-   * description will be aligned to the left, while the control itself will be
-   * aligned to the right.
-   */
-  inline?: boolean;
-  /**
-   * When stacking forms the bottom border is hidden and padding is adjusted
-   * for form elements to be stacked on each other.
-   */
-  stacked?: boolean;
-}
+import {FieldGroupProps} from './types';
+
+type FieldWrapperProps = Pick<
+  FieldGroupProps,
+  'hasControlState' | 'highlighted' | 'inline' | 'stacked'
+>;
 
 const inlineStyle = (p: FieldWrapperProps) =>
   p.inline

+ 34 - 119
static/app/components/forms/field/index.tsx

@@ -1,102 +1,20 @@
 import QuestionTooltip from 'sentry/components/questionTooltip';
 
-import ControlState, {ControlStateProps} from './controlState';
-import FieldControl, {FieldControlProps} from './fieldControl';
+import ControlState from './controlState';
+import FieldControl from './fieldControl';
 import FieldDescription from './fieldDescription';
 import FieldErrorReason from './fieldErrorReason';
 import FieldHelp from './fieldHelp';
 import FieldLabel from './fieldLabel';
 import FieldQuestion from './fieldQuestion';
 import FieldRequiredBadge from './fieldRequiredBadge';
-import FieldWrapper, {FieldWrapperProps} from './fieldWrapper';
+import FieldWrapper from './fieldWrapper';
+import {FieldGroupProps} from './types';
 
-interface InheritedFieldWrapperProps
-  extends Pick<
-    FieldWrapperProps,
-    'inline' | 'stacked' | 'highlighted' | 'hasControlState'
-  > {}
-
-interface InheritedFieldControlProps
-  extends Omit<
-    FieldControlProps,
-    'children' | 'disabled' | 'className' | 'help' | 'errorState'
-  > {}
-
-interface InheritedControlStateProps
-  extends Omit<ControlStateProps, 'children' | 'error'> {}
-
-export interface FieldProps
-  extends InheritedFieldControlProps,
-    InheritedFieldWrapperProps,
-    InheritedControlStateProps {
-  // TODO(TS): Do we need this?
-  /**
-   * The control to render. May be given a function to render with resolved
-   * props.
-   */
-  children?: React.ReactNode | ((props: ChildRenderProps) => React.ReactNode);
-  /**
-   * The classname of the field
-   */
-  className?: string;
-  /**
-   * The classname of the field control
-   */
-  controlClassName?: string;
-  /**
-   * Should field be disabled?
-   */
-  disabled?: boolean | ((props: FieldProps) => boolean);
-  /**
-   * Error message to display for the field
-   */
-  error?: string;
-  /**
-   * Help or description of the field
-   */
-  help?: React.ReactNode | React.ReactElement | ((props: FieldProps) => React.ReactNode);
-  /**
-   * Should the label be rendered for the field?
-   */
-  hideLabel?: boolean;
-  /**
-   * The control's `id` property
-   */
-  id?: string;
-  /**
-   * User-facing field name
-   */
-  label?: React.ReactNode;
-  /**
-   * May be used to give the field an aria-label when the field's label is a
-   * react node.
-   */
-  labelText?: string;
-  /**
-   * Show "required" indicator
-   */
-  required?: boolean;
-  /**
-   * Displays the help element in the tooltip
-   */
-  showHelpInTooltip?: boolean;
-  /**
-   * Additional inline styles for the field
-   */
-  style?: React.CSSProperties;
-  validate?: Function;
-  /**
-   * Should field be visible
-   */
-  visible?: boolean | ((props: FieldProps) => boolean);
-}
-
-interface ChildRenderProps extends Omit<FieldProps, 'className' | 'disabled'> {
-  controlState: React.ReactNode;
-  errorState: React.ReactNode | null;
-  help: React.ReactNode;
-  disabled?: boolean;
-}
+/**
+ * XXX: BC with getsentry
+ */
+export type FieldProps = FieldGroupProps;
 
 /**
  * A component to render a Field (i.e. label + help + form "control"),
@@ -106,68 +24,65 @@ interface ChildRenderProps extends Omit<FieldProps, 'className' | 'disabled'> {
  */
 function Field({
   className,
-  alignRight = false,
-  inline = true,
   disabled = false,
-  required = false,
+  inline = true,
   visible = true,
-  showHelpInTooltip = false,
-  ...props
-}: FieldProps) {
-  const otherProps = {
-    alignRight,
+  ...rest
+}: FieldGroupProps) {
+  const props = {
     inline,
     disabled,
-    required,
     visible,
-    showHelpInTooltip,
-    ...props,
+    ...rest,
   };
 
-  const isVisible = typeof visible === 'function' ? visible(otherProps) : visible;
-  const isDisabled = typeof disabled === 'function' ? disabled(otherProps) : disabled;
-
-  if (!isVisible) {
-    return null;
-  }
-
   const {
+    alignRight,
+    children,
     controlClassName,
-    highlighted,
     disabledReason,
     error,
     flexibleControlStateSize,
     help,
+    hideLabel,
+    highlighted,
     id,
-    isSaving,
     isSaved,
+    isSaving,
     label,
     labelText,
-    hideLabel,
+    required,
+    showHelpInTooltip,
     stacked,
-    children,
     style,
-  } = otherProps;
+  } = props;
+
+  const isVisible = typeof visible === 'function' ? visible(props) : visible;
+  const isDisabled = typeof disabled === 'function' ? disabled(props) : disabled;
 
-  const helpElement = typeof help === 'function' ? help(otherProps) : help;
+  if (!isVisible) {
+    return null;
+  }
+
+  const helpElement = typeof help === 'function' ? help(props) : help;
   const shouldRenderLabel = !hideLabel && !!label;
 
   const controlProps = {
-    className: controlClassName,
     inline,
     alignRight,
-    disabled: isDisabled,
     disabledReason,
     flexibleControlStateSize,
-    help: helpElement,
+    controlState: <ControlState error={!!error} isSaving={isSaving} isSaved={isSaved} />,
     errorState: error ? <FieldErrorReason>{error}</FieldErrorReason> : null,
-    controlState: <ControlState error={error} isSaving={isSaving} isSaved={isSaved} />,
+    className: controlClassName,
+    disabled: isDisabled,
+    help: helpElement,
   };
 
   // See comments in prop types
   const control =
     typeof children === 'function' ? (
-      children({...otherProps, ...controlProps})
+      children({...props, ...controlProps})
     ) : (
       <FieldControl {...controlProps}>{children}</FieldControl>
     );

+ 142 - 0
static/app/components/forms/field/types.tsx

@@ -0,0 +1,142 @@
+/**
+ * Props the control UI elements that are part of a Form Group
+ */
+export interface FieldGroupProps {
+  /**
+   * Align the control towards the right
+   */
+  alignRight?: boolean;
+  /**
+   * The control to render. May be given a function to render with resolved
+   * props.
+   */
+  children?: React.ReactNode | ((props: ChildRenderProps) => React.ReactNode);
+  /**
+   * The classname of the field
+   */
+  className?: string;
+  /**
+   * The classname of the field control
+   */
+  controlClassName?: string;
+  /**
+   * Loading / Saving / Error states of the form. See the ControlState
+   */
+  controlState?: React.ReactNode;
+  /**
+   * Should field be disabled?
+   */
+  disabled?: boolean | ((props: FieldGroupProps) => boolean);
+  /**
+   * Produces a question tooltip on the field, explaining why it is disabled
+   */
+  disabledReason?: React.ReactNode;
+  /**
+   * Display the  error indicator
+   */
+  error?: string | boolean;
+  /**
+   * The error state. Will not be rendered if hideControlState is true
+   */
+  errorState?: React.ReactNode;
+  /**
+   * Allow the control state to flex based on its content. When enabled the
+   * control state element will NOT take up space unless it has some state to
+   * show (such as an error).
+   */
+  flexibleControlStateSize?: boolean;
+  /**
+   * When false adds padding to the right of the element to ensure visual
+   * consistency with other fields that aren't using flexible control states.
+   */
+  hasControlState?: boolean;
+  /**
+   * Help or description of the field
+   */
+  help?: React.ReactNode | ((props: FieldGroupProps) => React.ReactNode);
+  /**
+   * Hide the fields control state
+   */
+  hideControlState?: boolean;
+  /**
+   * Should the label be rendered for the field?
+   */
+  hideLabel?: boolean;
+  /**
+   * Is "highlighted", i.e. after a search
+   */
+  highlighted?: boolean;
+  /**
+   * The control's `id` property
+   */
+  id?: string;
+  /**
+   * Display the field control container in "inline" fashion. The label and
+   * description will be aligned to the left, while the control itself will be
+   * aligned to the right.
+   *
+   * @default true
+   */
+  inline?: boolean;
+  /**
+   * Display the "was just saved" state
+   */
+  isSaved?: boolean;
+  /**
+   * Display the saving state
+   */
+  isSaving?: boolean;
+  /**
+   * User-facing field name
+   */
+  label?: React.ReactNode;
+  /**
+   * May be used to give the field an aria-label when the field's label is a
+   * complex react node.
+   */
+  labelText?: string;
+  /**
+   * Show indication that the field is required
+   */
+  required?: boolean;
+  /**
+   * Displays the help element in the tooltip
+   */
+  showHelpInTooltip?: boolean;
+  /**
+   * When stacking forms the bottom border is hidden and padding is adjusted
+   * for form elements to be stacked on each other.
+   */
+  stacked?: boolean;
+  /**
+   * Additional inline styles for the field
+   */
+  style?: React.CSSProperties;
+  /**
+   * Should field be visible
+   */
+  visible?: boolean | ((props: FieldGroupProps) => boolean);
+}
+
+/**
+ * The children render props mostly pass down FieldGroupProps, with some slight
+ * differences for properities that were resolved.
+ */
+interface ChildRenderProps extends Omit<FieldGroupProps, 'className' | 'disabled'> {
+  /**
+   * Same as {@link FieldGroupProps.controlState}, but will always be defined
+   */
+  controlState: React.ReactNode;
+  /**
+   * The error state may be null if there is no error
+   */
+  errorState: React.ReactNode | null;
+  /**
+   * The rendered help node
+   */
+  help: React.ReactNode;
+  /**
+   * Is the field disabled
+   */
+  disabled?: boolean;
+}

+ 2 - 2
static/app/components/forms/fieldFromConfig.tsx

@@ -1,4 +1,4 @@
-import {FieldProps} from 'sentry/components/forms/field';
+import {FieldGroupProps} from 'sentry/components/forms/field/types';
 import SeparatorField from 'sentry/components/forms/fields/separatorField';
 import {Field} from 'sentry/components/forms/types';
 import {Scope} from 'sentry/types';
@@ -54,7 +54,7 @@ function FieldFromConfig(props: FieldFromConfigProps): React.ReactElement | null
     case 'range':
       return <RangeField {...(componentProps as RangeFieldProps)} />;
     case 'blank':
-      return <BlankField {...(componentProps as FieldProps)} />;
+      return <BlankField {...(componentProps as FieldGroupProps)} />;
     case 'bool':
     case 'boolean':
       return <BooleanField {...(componentProps as BooleanFieldProps)} />;

Some files were not shown because too many files changed in this diff