diff --git a/eslint.config.ts b/eslint.config.ts index 323d3a70330f78..8c31cf017df12c 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1406,6 +1406,8 @@ export default typescript.config([ ...(enableTypeAwareLinting && { '@typescript-eslint/no-unsafe-argument': 'error', '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-enum-comparison': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', }), }, diff --git a/static/app/components/core/compactSelect/gridList/index.tsx b/static/app/components/core/compactSelect/gridList/index.tsx index 0353809e9e78de..df495c58031f80 100644 --- a/static/app/components/core/compactSelect/gridList/index.tsx +++ b/static/app/components/core/compactSelect/gridList/index.tsx @@ -14,6 +14,7 @@ import { SizeLimitMessage, useVirtualizedItems, } from '@sentry/scraps/compactSelect'; +import type {ListItemBase} from '@sentry/scraps/compactSelect/types'; import {Container} from '@sentry/scraps/layout'; import {t} from 'sentry/locale'; @@ -21,7 +22,7 @@ import {t} from 'sentry/locale'; import {GridListOption, type GridListOptionProps} from './option'; import {GridListSection} from './section'; -interface GridListProps +interface GridListProps extends Omit, 'children'>, Omit< @@ -43,13 +44,13 @@ interface GridListProps * Object containing the selection state and focus position, needed for * `useGridList()`. */ - listState: ListState; - children?: CollectionChildren; + listState: ListState; + children?: CollectionChildren; /** * Text label to be rendered as heading on top of grid list. */ label?: React.ReactNode; - size?: GridListOptionProps['size']; + size?: GridListOptionProps['size']; /** * Message to be displayed when some options are hidden due to `sizeLimit`. */ @@ -70,7 +71,7 @@ interface GridListProps * inside. Grid lists allow users to focus on those child elements (using the Arrow * Left/Right keys) and interact with them, which isn't possible with list boxes. */ -function GridList({ +function GridList({ listState, size = 'md', label, @@ -78,7 +79,7 @@ function GridList({ keyDownHandler, virtualized, ...props -}: GridListProps) { +}: GridListProps) { const ref = useRef(null); const labelId = useId(); const {gridProps} = useGridList( diff --git a/static/app/components/core/compactSelect/gridList/option.tsx b/static/app/components/core/compactSelect/gridList/option.tsx index 676b6b0df03988..50fece393cddf4 100644 --- a/static/app/components/core/compactSelect/gridList/option.tsx +++ b/static/app/components/core/compactSelect/gridList/option.tsx @@ -9,6 +9,7 @@ import type {Node} from '@react-types/shared'; import {Checkbox} from '@sentry/scraps/checkbox'; import {LeadWrap} from '@sentry/scraps/compactSelect'; +import type {ListItemBase} from '@sentry/scraps/compactSelect/types'; import { InnerWrap, MenuListItem, @@ -18,9 +19,11 @@ import { import {IconCheckmark} from 'sentry/icons'; import type {FormSize} from 'sentry/utils/theme'; -export interface GridListOptionProps extends AriaGridListItemOptions { - listState: ListState; - node: Node; +export interface GridListOptionProps< + T extends ListItemBase, +> extends AriaGridListItemOptions { + listState: ListState; + node: Node; size: FormSize; } @@ -28,7 +31,11 @@ export interface GridListOptionProps extends AriaGridListItemOptions { * A
  • element with accessibile behaviors & attributes. * https://react-spectrum.adobe.com/react-aria/useGridList.html */ -export function GridListOption({node, listState, size}: GridListOptionProps) { +export function GridListOption({ + node, + listState, + size, +}: GridListOptionProps) { const ref = useRef(null); const { label, @@ -73,7 +80,7 @@ export function GridListOption({node, listState, size}: GridListOptionProps) { [label] ); - const leadingItems: MenuListItemProps['leadingItems'] = node.props.leadingItems; + const leadingItems = (node.props as MenuListItemProps).leadingItems; const leadingItemsMemo = useMemo(() => { const checkboxSize = size === 'xs' ? 'xs' : 'sm'; diff --git a/static/app/components/core/compactSelect/gridList/section.tsx b/static/app/components/core/compactSelect/gridList/section.tsx index 228c1295e65e8f..838b05cc484919 100644 --- a/static/app/components/core/compactSelect/gridList/section.tsx +++ b/static/app/components/core/compactSelect/gridList/section.tsx @@ -12,26 +12,31 @@ import { SectionWrap, SelectFilterContext, } from '@sentry/scraps/compactSelect'; +import type {ListItemBase} from '@sentry/scraps/compactSelect/types'; import {GridListOption, type GridListOptionProps} from './option'; -interface GridListSectionProps { - listState: ListState; - node: Node; - size: GridListOptionProps['size']; +interface GridListSectionProps { + listState: ListState; + node: Node; + size: GridListOptionProps['size']; } /** * A
  • element that functions as a grid list section (renders a nested
      * inside). https://react-spectrum.adobe.com/react-aria/useGridList.html */ -export function GridListSection({node, listState, size}: GridListSectionProps) { +export function GridListSection({ + node, + listState, + size, +}: GridListSectionProps) { const titleId = useId(); const {separatorProps} = useSeparator({elementType: 'li'}); const showToggleAllButton = listState.selectionManager.selectionMode === 'multiple' && - node.value.showToggleAllButton; + node.value?.showToggleAllButton; const hiddenOptions = useContext(SelectFilterContext); const childNodes = useMemo( diff --git a/static/app/components/core/compactSelect/list.tsx b/static/app/components/core/compactSelect/list.tsx index df69c281a15d3d..f15f844f0cf2b9 100644 --- a/static/app/components/core/compactSelect/list.tsx +++ b/static/app/components/core/compactSelect/list.tsx @@ -12,6 +12,7 @@ import {ControlContext} from './control'; import {GridList} from './gridList'; import {ListBox} from './listBox'; import type { + ListItemBase, SelectKey, SelectOption, SelectOptionOrSectionWithKey, @@ -184,7 +185,7 @@ export function List({ /** * Props to be passed into useListState() */ - const listStateProps = useMemo>>(() => { + const listStateProps = useMemo>>(() => { const disabledKeys = [ ...getDisabledOptions(items, isOptionDisabled), ...hiddenOptions, @@ -374,7 +375,7 @@ export function List({ )} {multiple && sections.map(section => - section.value.showToggleAllButton ? ( + section.value?.showToggleAllButton ? ( requires an index signature -// eslint-disable-next-line @typescript-eslint/no-restricted-types -type ObjectLike = object; - -interface ListBoxProps +interface ListBoxProps extends Omit< React.HTMLAttributes, @@ -122,7 +119,7 @@ const DEFAULT_KEY_DOWN_HANDLER = () => true; * If interactive children are necessary, consider using grid lists instead (by setting * the `grid` prop on CompactSelect to true). */ -export function ListBox({ +export function ListBox({ ref, listState, size = 'md', @@ -296,7 +293,7 @@ const heightEstimations = { */ const listPaddingVertical = 4; -function useVirtualizedItems({ +function useVirtualizedItems({ listItems, virtualized = false, size, @@ -315,6 +312,7 @@ function useVirtualizedItems({ getScrollElement: () => scrollElementRef?.current, estimateSize: index => { const item = listItems[index]; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (item?.props?.details) { return heightEstimation.large; } diff --git a/static/app/components/core/compactSelect/listBox/option.tsx b/static/app/components/core/compactSelect/listBox/option.tsx index 7cfb7b1d794f71..3c39e6d5f98ee5 100644 --- a/static/app/components/core/compactSelect/listBox/option.tsx +++ b/static/app/components/core/compactSelect/listBox/option.tsx @@ -65,7 +65,7 @@ export function ListBoxOption({ [labelProps.id, label] ); - const leadingItems: MenuListItemProps['leadingItems'] = item.props.leadingItems; + const leadingItems = (item.props as MenuListItemProps).leadingItems; const leadingItemsMemo = useMemo(() => { const checkboxSize = size === 'xs' ? 'xs' : 'sm'; diff --git a/static/app/components/core/compactSelect/listBox/section.tsx b/static/app/components/core/compactSelect/listBox/section.tsx index 66a9e5430dbe21..a301b445df2de0 100644 --- a/static/app/components/core/compactSelect/listBox/section.tsx +++ b/static/app/components/core/compactSelect/listBox/section.tsx @@ -14,13 +14,14 @@ import { SectionWrap, } from '@sentry/scraps/compactSelect'; import type {SelectKey} from '@sentry/scraps/compactSelect'; +import type {ListItemBase} from '@sentry/scraps/compactSelect/types'; import {ListBoxOption, type ListBoxOptionProps} from './option'; -interface ListBoxSectionProps extends AriaListBoxSectionProps { +interface ListBoxSectionProps extends AriaListBoxSectionProps { hiddenOptions: Set; - item: Node; - listState: ListState; + item: Node; + listState: ListState; showSectionHeaders: boolean; size: ListBoxOptionProps['size']; 'data-index'?: number; @@ -32,7 +33,7 @@ interface ListBoxSectionProps extends AriaListBoxSectionProps { * A
    • element that functions as a list box section (renders a nested
        * inside). https://react-spectrum.adobe.com/react-aria/useListBox.html */ -export function ListBoxSection({ +export function ListBoxSection({ item, listState, size, @@ -41,7 +42,7 @@ export function ListBoxSection({ showDetails = true, ref, 'data-index': dataIndex, -}: ListBoxSectionProps) { +}: ListBoxSectionProps) { const {itemProps, headingProps, groupProps} = useListBoxSection({ heading: item.rendered, 'aria-label': item['aria-label'], @@ -51,7 +52,7 @@ export function ListBoxSection({ const showToggleAllButton = listState.selectionManager.selectionMode === 'multiple' && - item.value.showToggleAllButton; + item.value?.showToggleAllButton; const childItems = useMemo( () => [...item.childNodes].filter(child => !hiddenOptions.has(child.key)), diff --git a/static/app/components/core/compactSelect/types.tsx b/static/app/components/core/compactSelect/types.tsx index 5a33ae55542cb2..5bad32ef9e217e 100644 --- a/static/app/components/core/compactSelect/types.tsx +++ b/static/app/components/core/compactSelect/types.tsx @@ -1,5 +1,8 @@ import type {SelectValue} from 'sentry/types/core'; +// explicitly using object here because Record requires an index signature +// eslint-disable-next-line @typescript-eslint/no-restricted-types +export type ListItemBase = object & {showToggleAllButton?: boolean}; export type SelectKey = string | number; export interface SelectOption extends SelectValue { diff --git a/static/app/components/core/layout/container.tsx b/static/app/components/core/layout/container.tsx index 3f939d44b2edce..b6855d3983b08b 100644 --- a/static/app/components/core/layout/container.tsx +++ b/static/app/components/core/layout/container.tsx @@ -241,11 +241,13 @@ const omitContainerProps = new Set([ export const Container = styled( ( - props: ContainerProps | ContainerPropsWithRenderFunction + props: (ContainerProps | ContainerPropsWithRenderFunction) & { + className?: string; + } ) => { if (typeof props.children === 'function') { // When using render prop, only pass className to the child function - return props.children({className: (props as any).className}); + return props.children({className: props.className ?? ''}); } const {as, ...rest} = props; diff --git a/static/app/components/core/layout/surface.tsx b/static/app/components/core/layout/surface.tsx index 1ee39900f9298c..2f0f0929221caa 100644 --- a/static/app/components/core/layout/surface.tsx +++ b/static/app/components/core/layout/surface.tsx @@ -83,16 +83,17 @@ function isRenderFunction( export const Surface = styled( ( - props: + props: ( | SurfaceProps | FlatSurfacePropsWithRenderFunction | OverlaySurfacePropsWithRenderFunction + ) & {className?: string} ) => { if (isRenderFunction(props)) { // When using render prop, only pass className to the child function. T // The className in this case is internally generated by emotion, and not part of the // passed props. - return props.children({className: (props as any).className ?? ''}); + return props.children({className: props.className ?? ''}); } const {variant, elevation: _, ...rest} = props; diff --git a/static/app/components/core/segmentedControl/segmentedControl.tsx b/static/app/components/core/segmentedControl/segmentedControl.tsx index 3f6c73423bd7c7..4f7a0624618fa5 100644 --- a/static/app/components/core/segmentedControl/segmentedControl.tsx +++ b/static/app/components/core/segmentedControl/segmentedControl.tsx @@ -138,6 +138,7 @@ export function SegmentedControl({ nextKey={option.nextKey} prevKey={option.prevKey} value={String(option.key)} + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access isDisabled={option.props.disabled || disabled} state={state} size={size} diff --git a/static/app/components/core/select/option.tsx b/static/app/components/core/select/option.tsx index 4ba6daab332d5e..3428d94b1c3054 100644 --- a/static/app/components/core/select/option.tsx +++ b/static/app/components/core/select/option.tsx @@ -57,9 +57,11 @@ export function SelectOption(props: Props) { innerWrapProps={{'data-test-id': value}} labelProps={{as: typeof label === 'string' ? 'p' : 'div'}} leadingItems={ + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access itemProps.__isNew__ ? ( + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */} {data.leadingItems} ) : ( @@ -72,6 +74,7 @@ export function SelectOption(props: Props) { /> )} + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */} {data.leadingItems} ) diff --git a/static/app/components/core/select/select.tsx b/static/app/components/core/select/select.tsx index 4d26d276502dea..7b603a3b365067 100644 --- a/static/app/components/core/select/select.tsx +++ b/static/app/components/core/select/select.tsx @@ -79,7 +79,7 @@ const getStylesConfig = ({ // Unfortunately we cannot use emotions `css` helper here, since react-select // *requires* object styles, which the css helper cannot produce. const indicatorStyles: StylesConfig['clearIndicator'] & - StylesConfig['loadingIndicator'] = (provided, state: any) => ({ + StylesConfig['loadingIndicator'] = (provided, state: {isDisabled?: boolean}) => ({ ...provided, padding: '0 4px 0 4px', alignItems: 'center', @@ -639,13 +639,16 @@ export function Select !!opt.disabled} showDividers={props.showDividers} options={options || (choicesOrOptions as OptionsType)} openMenuOnFocus={props.openMenuOnFocus} + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access blurInputOnSelect={!props.multiple && !anyProps.multi} + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access closeMenuOnSelect={!(props.multiple || anyProps.multi)} hideSelectedOptions={false} tabSelectsValue={false} diff --git a/static/app/components/core/tabs/tabList.tsx b/static/app/components/core/tabs/tabList.tsx index 9dc606320a5342..f51041e501e124 100644 --- a/static/app/components/core/tabs/tabList.tsx +++ b/static/app/components/core/tabs/tabList.tsx @@ -254,21 +254,23 @@ function BaseTabList({outerWrapStyles, variant = 'flat', ...props}: BaseTabListP (a, b) => sortedKeys.indexOf(a) - sortedKeys.indexOf(b) ); - return sortedOverflowTabs.flatMap>(key => { + return sortedOverflowTabs.flatMap(key => { const item = state.collection.getItem(key); if (!item) { return []; } + const itemProps: TabListItemProps = item.props; + return { value: key, - label: item.props.children, - disabled: item.props.disabled, - tooltip: item.props.tooltip?.title, - tooltipOptions: item.props.tooltip, + label: itemProps.children, + disabled: itemProps.disabled, + tooltip: itemProps.tooltip?.title, + tooltipOptions: itemProps.tooltip, textValue: item.textValue, - }; + } satisfies SelectOption; }); }, [state.collection, overflowTabs]); @@ -288,7 +290,7 @@ function BaseTabList({outerWrapStyles, variant = 'flat', ...props}: BaseTabListP orientation={orientation} size={size} overflowing={orientation === 'horizontal' && overflowTabs.includes(item.key)} - tooltipProps={item.props.tooltip} + tooltipProps={(item.props as TabListItemProps).tooltip} ref={element => { tabItemsRef.current[item.key] = element; }} diff --git a/static/app/components/core/tabs/tabPanels.tsx b/static/app/components/core/tabs/tabPanels.tsx index 862cc6273ff3b5..9299c47c760259 100644 --- a/static/app/components/core/tabs/tabPanels.tsx +++ b/static/app/components/core/tabs/tabPanels.tsx @@ -49,6 +49,7 @@ export function TabPanels(props: TabPanelsProps) { orientation={orientation} key={tabListState?.selectedKey} > + {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */} {selectedPanel?.props.children} ); diff --git a/static/app/components/core/text/heading.tsx b/static/app/components/core/text/heading.tsx index 2fadf3be7f2a77..b588e87712885e 100644 --- a/static/app/components/core/text/heading.tsx +++ b/static/app/components/core/text/heading.tsx @@ -56,10 +56,10 @@ export type HeadingPropsWithRenderFunction = BaseHeadingProps & >; export const Heading = styled( - (props: HeadingProps | HeadingPropsWithRenderFunction) => { + (props: (HeadingProps | HeadingPropsWithRenderFunction) & {className?: string}) => { if (typeof props.children === 'function') { // When using render prop, only pass className to the child function - return props.children({className: (props as any).className}); + return props.children({className: props.className ?? ''}); } const {children, as, ...rest} = props as HeadingProps; const HeadingComponent = as; diff --git a/static/app/components/core/text/text.tsx b/static/app/components/core/text/text.tsx index fe918b898c6c81..5f69583e218674 100644 --- a/static/app/components/core/text/text.tsx +++ b/static/app/components/core/text/text.tsx @@ -184,11 +184,11 @@ export type TextPropsWithRenderFunction = export const Text = styled( ( - props: TextProps | TextPropsWithRenderFunction + props: (TextProps | TextPropsWithRenderFunction) & {className?: string} ) => { if (typeof props.children === 'function') { // When using render prop, only pass className to the child function - return props.children({className: (props as any).className}); + return props.children({className: props.className ?? ''}); } const {children, ...rest} = props as TextProps; const Component = props.as || 'span'; diff --git a/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx b/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx index 25aea3bfb60597..ef8fb192d73b60 100644 --- a/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx +++ b/static/app/components/modals/widgetBuilder/addToDashboardModal.tsx @@ -384,24 +384,27 @@ function AddToDashboardModal({ disabled: hasReachedDashboardLimit || isLoading, tooltip: hasReachedDashboardLimit ? limitMessage : undefined, tooltipOptions: {position: 'right', isHoverable: true}, - }, + } satisfies SelectValue, ...dashboards .filter(dashboard => // if adding from a dashboard, currentDashboardId will be set and we'll remove it from the list of options currentDashboardId ? dashboard.id !== currentDashboardId : true ) - .map(({title, id, widgetDisplay}) => ({ - label: title, - value: id, - disabled: widgetDisplay.length + widgets.length >= MAX_WIDGETS, - tooltip: - widgetDisplay.length + widgets.length >= MAX_WIDGETS && - tct('Max widgets ([maxWidgets]) per dashboard reached.', { - maxWidgets: MAX_WIDGETS, - }), - tooltipOptions: {position: 'right'}, - })), - ].filter(Boolean) as Array>; + .map( + ({title, id, widgetDisplay}) => + ({ + label: title, + value: id, + disabled: widgetDisplay.length + widgets.length >= MAX_WIDGETS, + tooltip: + widgetDisplay.length + widgets.length >= MAX_WIDGETS && + tct('Max widgets ([maxWidgets]) per dashboard reached.', { + maxWidgets: MAX_WIDGETS, + }), + tooltipOptions: {position: 'right'}, + }) satisfies SelectValue + ), + ].filter(Boolean); }, [currentDashboardId, dashboards, widgets.length] ); diff --git a/static/app/components/modals/widgetBuilder/linkToDashboardModal.tsx b/static/app/components/modals/widgetBuilder/linkToDashboardModal.tsx index a079286c0a633d..dbdceeb06c6204 100644 --- a/static/app/components/modals/widgetBuilder/linkToDashboardModal.tsx +++ b/static/app/components/modals/widgetBuilder/linkToDashboardModal.tsx @@ -149,24 +149,27 @@ export function LinkToDashboardModal({ disabled: hasReachedDashboardLimit || isLoading, tooltip: hasReachedDashboardLimit ? limitMessage : undefined, tooltipOptions: {position: 'right', isHoverable: true}, - }, + } satisfies SelectValue, ...dashboards .filter(dashboard => // if adding from a dashboard, currentDashboardId will be set and we'll remove it from the list of options currentDashboardId ? dashboard.id !== currentDashboardId : true ) - .map(({title, id, widgetDisplay}) => ({ - label: title, - value: id, - disabled: widgetDisplay.length >= MAX_WIDGETS, - tooltip: - widgetDisplay.length >= MAX_WIDGETS && - tct('Max widgets ([maxWidgets]) per dashboard reached.', { - maxWidgets: MAX_WIDGETS, - }), - tooltipOptions: {position: 'right'}, - })), - ].filter(Boolean) as Array>; + .map( + ({title, id, widgetDisplay}) => + ({ + label: title, + value: id, + disabled: widgetDisplay.length >= MAX_WIDGETS, + tooltip: + widgetDisplay.length >= MAX_WIDGETS && + tct('Max widgets ([maxWidgets]) per dashboard reached.', { + maxWidgets: MAX_WIDGETS, + }), + tooltipOptions: {position: 'right'}, + }) satisfies SelectValue + ), + ].filter(Boolean); }, [currentDashboardId, dashboards] ); diff --git a/static/app/utils/useOwnerOptions.tsx b/static/app/utils/useOwnerOptions.tsx index fed242ffbc1580..81f30b46614ffa 100644 --- a/static/app/utils/useOwnerOptions.tsx +++ b/static/app/utils/useOwnerOptions.tsx @@ -4,6 +4,7 @@ import type {AvatarProps} from '@sentry/scraps/avatar'; import {TeamAvatar, UserAvatar} from '@sentry/scraps/avatar'; import {t} from 'sentry/locale'; +import type {SelectValue} from 'sentry/types/core'; import type {DetailedTeam, Team} from 'sentry/types/organization'; import type {User} from 'sentry/types/user'; @@ -40,11 +41,14 @@ export function useOwnerOptions({ // frustratingly that is difficult likely because we're recreating this // object on every re-render. const memberOptions = - members?.map(member => ({ - value: `user:${member.id}`, - label: member.name, - leadingItems: , - })) ?? []; + members?.map( + member => + ({ + value: `user:${member.id}`, + label: member.name, + leadingItems: , + }) satisfies SelectValue + ) ?? []; const makeTeamOption = (team: Team) => ({ value: `team:${team.id}`, @@ -52,12 +56,13 @@ export function useOwnerOptions({ leadingItems: , }); - const makeDisabledTeamOption = (team: Team) => ({ - ...makeTeamOption(team), - disabled: true, - tooltip: t('%s is not a member of the selected projects', `#${team.slug}`), - tooltipOptions: {position: 'left'}, - }); + const makeDisabledTeamOption = (team: Team) => + ({ + ...makeTeamOption(team), + disabled: true, + tooltip: t('%s is not a member of the selected projects', `#${team.slug}`), + tooltipOptions: {position: 'left'}, + }) satisfies SelectValue; const {disabledTeams, memberTeams, otherTeams} = groupBy( teams as DetailedTeam[], diff --git a/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx b/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx index d058c939142b31..f0ebc7b0136569 100644 --- a/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/visualize/selectRow.tsx @@ -67,7 +67,7 @@ interface SelectRowProps { } function validateParameter( - columnOptions: Array>, + columnOptions: Array<{value: string}>, parameter: AggregateParameter, value: string | undefined ) {