Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1393,4 +1393,12 @@ export default typescript.config([
'boundaries/dependencies': 'off',
},
},
{
name: 'files/scraps',
files: ['static/app/components/core/**/*.{js,mjs,ts,jsx,tsx}'],
ignores: ['**/*.spec.{js,mjs,ts,jsx,tsx}'],
rules: {
'@typescript-eslint/no-non-null-assertion': 'error',
},
},
]);
2 changes: 2 additions & 0 deletions static/app/components/core/avatar/avatarList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@ export function AvatarList({

if (numCollapsedAvatars === 1) {
if (visibleTeamAvatars.length < teams.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
visibleTeamAvatars.unshift(teams[teams.length - 1]!);
} else if (visibleUserAvatars.length < users.length) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
visibleUserAvatars.unshift(users[users.length - 1]!);
}
numCollapsedAvatars = 0;
Expand Down
5 changes: 3 additions & 2 deletions static/app/components/core/avatar/useAvatar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,10 @@ function getInitials(name: string | undefined): Tagged<string, '__avatar'> {

// Use Array.from as slicing and substring() work on ucs2 segments which
// results in only getting half of any 4+ byte character.
let initials = Array.from(words[0]!)[0]!;

let initials = Array.from(words[0] ?? '')[0] ?? '';
if (words.length > 1) {
initials += Array.from(words[words.length - 1]!)[0]!;
initials += Array.from(words[words.length - 1] ?? '')[0] ?? '';
}
return initials.toUpperCase() as Tagged<string, '__avatar'>;
}
24 changes: 17 additions & 7 deletions static/app/components/core/avatarButton/avatarButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useTheme, type Theme} from '@emotion/react';
import styled from '@emotion/styled';
import {useQuery} from '@tanstack/react-query';
import {skipToken, useQuery} from '@tanstack/react-query';
import color from 'color';

import type {BaseAvatarProps} from '@sentry/scraps/avatar';
Expand Down Expand Up @@ -32,8 +32,10 @@ export function AvatarButton({avatar, size: explicitSize, ...props}: AvatarButto

const {data: imageResult} = useQuery({
queryKey: ['avatar-button-chonk', imageUrl, theme.type],
queryFn: () => resolveImageAvatarColors(imageUrl!, theme.type),
enabled: !!imageUrl && avatarDefinition.type === 'image',
queryFn:
imageUrl && avatarDefinition.type === 'image'
? () => resolveImageAvatarColors(imageUrl, theme.type)
: skipToken,
staleTime: Infinity,
});

Expand Down Expand Up @@ -133,6 +135,7 @@ const StyledAvatarButton = styled(Button)<{chonk: string | undefined}>`
// Returns 'fill' when the image covers the full frame edge-to-edge, 'padded' otherwise.
// Each edge check returns 'padded' when every pixel on that edge is transparent (alpha < 128).
// Pixel (col, row) has its alpha channel at (row * 12 + col) * 4 + 3 in a 12×12 RGBA canvas.
/* eslint-disable @typescript-eslint/no-non-null-assertion */
function shouldPadImage(data: Uint8ClampedArray): 'fill' | 'padded' {
// oxfmt-ignore
if (!(data[3]!>=128 || data[51]!>=128 || data[99]!>=128 ||
Expand All @@ -159,6 +162,7 @@ function shouldPadImage(data: Uint8ClampedArray): 'fill' | 'padded' {

return 'fill';
}
/* eslint-enable @typescript-eslint/no-non-null-assertion */

function readPixels(img: HTMLImageElement): Uint8ClampedArray | null {
const SAMPLE_SIZE = 12;
Expand Down Expand Up @@ -200,18 +204,24 @@ function sampleAvatarColor(
const style = shouldPadImage(data);

// Accumulate two sets: chromatic pixels (saturation ≥ 0.15) and all opaque pixels.
// oxfmt-ignore
let cr = 0, cg = 0, cb = 0, ccount = 0;
// oxfmt-ignore
let ar = 0, ag = 0, ab = 0, acount = 0;
let cr = 0,
cg = 0,
cb = 0,
ccount = 0;
let ar = 0,
ag = 0,
ab = 0,
acount = 0;

for (let i = 0; i < data.length; i += 4) {
/* eslint-disable @typescript-eslint/no-non-null-assertion */
if (data[i + 3]! < 128) continue;

const r = data[i]!,
g = data[i + 1]!,
b = data[i + 2]!;

/* eslint-enable @typescript-eslint/no-non-null-assertion */
// accumulate all pixels
ar += r;
ag += g;
Expand Down
4 changes: 2 additions & 2 deletions static/app/components/core/compactSelect/control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -504,13 +504,13 @@ export function Control({
ref={menuRef}
width={menuWidth ?? menuFullWidth}
height={menuHeight}
minWidth={menuMinWidth ?? overlayProps.style!.minWidth}
minWidth={menuMinWidth ?? overlayProps.style?.minWidth}
maxWidth={
overlayProps.style?.maxWidth
? `calc(${withUnits(overlayProps.style.maxWidth)} * 0.9)`
: undefined
}
maxHeight={overlayProps.style!.maxHeight}
maxHeight={overlayProps.style?.maxHeight}
maxHeightProp={maxMenuHeight}
data-menu-has-header={!!menuTitle || clearable}
data-menu-has-search={searchEnabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ function GridList({
ref={ref}
>
{virtualizer.items.map(row => {
const item = listItems[row.index]!;
const item = listItems[row.index];
if (!item) return null;
if (item.type === 'section') {
return (
<GridListSection
Expand Down
1 change: 1 addition & 0 deletions static/app/components/core/compactSelect/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ export function List<Value extends SelectKey>({
disallowEmptySelection: !clearable,
allowDuplicateSelectionEvents: true,
onSelectionChange: selection => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const selectedOption = getSelectedOptions(items, selection)[0]!;
onChange?.(selectedOption);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ export function ListBox<T extends ObjectLike>({
>
{overlayIsOpen &&
virtualizer.items.map(row => {
const item = listItems[row.index]!;
const item = listItems[row.index];
if (!item) return null;
if (item.type === 'section') {
return (
<ListBoxSection
Expand Down
24 changes: 13 additions & 11 deletions static/app/components/core/compactSelect/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,13 @@ export function getHiddenOptions<Value extends SelectKey>(
//
// Then, limit the number of remaining options to `limit`
//
let threshold = [Infinity, Infinity];
let threshold: [number, number] = [Infinity, Infinity];
let accumulator = 0;
let currentIndex = 0;

while (currentIndex < orderedRemainingItems.length) {
const item = orderedRemainingItems[currentIndex]!;
const delta = 'options' in item ? item.options.length : 1;
const item = orderedRemainingItems[currentIndex];
const delta = item && 'options' in item ? item.options.length : 1;

if (accumulator + delta > limit) {
threshold = [currentIndex, limit - accumulator];
Expand All @@ -233,15 +233,17 @@ export function getHiddenOptions<Value extends SelectKey>(
currentIndex += 1;
}

for (let i = threshold[0]!; i < orderedRemainingItems.length; i++) {
const item = orderedRemainingItems[i]!;
if ('options' in item) {
const startingIndex = i === threshold[0] ? threshold[1]! : 0;
for (let j = startingIndex; j < item.options.length; j++) {
hiddenOptionsSet.add(item.options[j]!.key);
for (let i = threshold[0]; i < orderedRemainingItems.length; i++) {
const item = orderedRemainingItems[i];
if (item) {
if ('options' in item) {
const startingIndex = i === threshold[0] ? threshold[1] : 0;
for (const option of item.options.slice(startingIndex)) {
hiddenOptionsSet.add(option.key);
}
} else {
hiddenOptionsSet.add(item.key);
}
} else {
hiddenOptionsSet.add(item.key);
}
}

Expand Down
2 changes: 1 addition & 1 deletion static/app/components/core/form/field/baseField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function useFocusRestore(ref: React.RefObject<HTMLElement | null>) {
}

function onBlur() {
if (el!.hasAttribute('disabled')) {
if (el?.hasAttribute('disabled')) {
hadFocusRef.current = true;
}
}
Expand Down
13 changes: 9 additions & 4 deletions static/app/components/core/layout/styles.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ThemeFixture} from 'sentry-fixture/theme';

import {act, renderHookWithProviders} from 'sentry-test/reactTestingLibrary';

import {assert} from 'sentry/types/utils';
import type {BreakpointSize} from 'sentry/utils/theme';

// eslint-disable-next-line boundaries/dependencies
Expand Down Expand Up @@ -58,7 +59,8 @@ const setupMediaQueries = (

describe('rc', () => {
it('returns a simple CSS declaration for a plain string value', () => {
const output = rc('color', 'red', theme)!;
const output = rc('color', 'red', theme);
assert(output);
expect(
normalizeCss(
css`
Expand All @@ -73,7 +75,8 @@ describe('rc', () => {
});

it('applies a resolver to a plain value', () => {
const output = rc('color', 'primary', theme, value => `resolved-${value}`)!;
const output = rc('color', 'primary', theme, value => `resolved-${value}`);
assert(output);
expect(
normalizeCss(
css`
Expand All @@ -89,7 +92,8 @@ describe('rc', () => {

it('generates media queries for responsive values', () => {
// First defined breakpoint gets both min-width and max-width; subsequent get min-width only.
const output = rc('color', {xs: 'blue', md: 'green'}, theme)!;
const output = rc('color', {xs: 'blue', md: 'green'}, theme);
assert(output);
expect(
normalizeCss(
css`
Expand All @@ -101,7 +105,8 @@ describe('rc', () => {

it('skips undefined intermediate breakpoints', () => {
// xs and md are defined; 2xs, sm, lg, xl, 2xl are absent from the output.
const output = rc('font-size', {xs: 'md', md: 'lg'}, theme)!;
const output = rc('font-size', {xs: 'md', md: 'lg'}, theme);
assert(output);
expect(
normalizeCss(
css`
Expand Down
23 changes: 8 additions & 15 deletions static/app/components/core/layout/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,10 @@ function resolveRadius(sizeComponent: RadiusSize | undefined, theme: Theme) {
}

function resolveSpacing(sizeComponent: SpaceSize, theme: Theme) {
if (sizeComponent === undefined) {
return;
}

return theme.space[sizeComponent] ?? theme.space['0'];
}

function resolveMargin(sizeComponent: Margin, theme: Theme) {
if (sizeComponent === undefined) {
return;
}

if (sizeComponent === 'auto') {
return 'auto';
}
Expand Down Expand Up @@ -209,7 +201,7 @@ export function getMargin(

if (margin.length < 3) {
// This can only be a single margin value, so we can resolve it directly.
return resolveMargin(margin as Margin, theme)!;
return resolveMargin(margin as Margin, theme);
}

return margin
Expand All @@ -226,12 +218,12 @@ export function getMargin(
type ResponsiveValue<T> = T extends Responsive<infer U> ? U : never;
export function useResponsivePropValue<T extends Responsive<any>>(
prop: T
): ResponsiveValue<T> {
): T | ResponsiveValue<T> {
const activeBreakpoint = useActiveBreakpoint();

// Only resolve the active breakpoint if the prop is responsive, else ignore it.
if (!isResponsive(prop)) {
return prop as unknown as ResponsiveValue<T>;
return prop;
}

if (Object.keys(prop).length === 0) {
Expand All @@ -249,8 +241,8 @@ export function useResponsivePropValue<T extends Responsive<any>>(

// If we don't have an exact match, find the next smallest breakpoint
for (let i = activeIndex - 1; i >= 0; i--) {
const smallerBreakpoint = BREAKPOINT_ORDER[i]!;
if (prop[smallerBreakpoint] !== undefined) {
const smallerBreakpoint = BREAKPOINT_ORDER[i];
if (smallerBreakpoint && prop[smallerBreakpoint] !== undefined) {
value = prop[smallerBreakpoint];
break;
}
Expand All @@ -259,14 +251,15 @@ export function useResponsivePropValue<T extends Responsive<any>>(
// If no smaller breakpoint found, then window < smallest breakpoint, so we need to find the first larger breakpoint
if (value === undefined) {
for (let i = activeIndex + 1; i < BREAKPOINT_ORDER.length; i++) {
const largerBreakpoint = BREAKPOINT_ORDER[i]!;
if (prop[largerBreakpoint] !== undefined) {
const largerBreakpoint = BREAKPOINT_ORDER[i];
if (largerBreakpoint && prop[largerBreakpoint] !== undefined) {
value = prop[largerBreakpoint];
break;
}
}
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return value!;
Comment on lines +262 to 263
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm can't we do something like?

if (value === undefined) {
  Sentry.captureMessage('useResponsivePropValue: no defined breakpoint resolved', {
      extra: {prop},
    });
    return undefined as ResponsiveValue<T>;
  }

  return value;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine as-is, but I addressed your other findings 🙏

}

Expand Down
2 changes: 1 addition & 1 deletion static/app/components/core/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ function isGroupedOptions<OptionType extends OptionTypeBase>(
if (!maybe || maybe.length === 0) {
return false;
}
return (maybe as GroupedOptionsType<OptionType>)[0]!.options !== undefined;
return (maybe as GroupedOptionsType<OptionType>)[0]?.options !== undefined;
}

function MultiValueRemove(
Expand Down
1 change: 1 addition & 0 deletions static/app/components/core/slider/slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function Slider({
state
);

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
useImperativeHandle(ref, () => inputRef.current!, []);

const thumbPercent = state.getThumbPercent(0);
Expand Down
Loading
Loading