Merge pull request #70 from cheir-mneme/fix/p4-frontend

fix(frontend): React quality improvements - keys, types, memoization
This commit is contained in:
Cody Kickertz 2025-12-19 14:39:40 -06:00 committed by GitHub
commit 32d072dd8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 70 additions and 57 deletions

View file

@ -25,8 +25,7 @@ export type SelectContextAction =
export type SelectDispatch = (action: SelectContextAction) => void;
interface SelectProviderOptions<T extends ModelBase> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
children: any;
children: React.ReactNode;
items: Array<T>;
}

View file

@ -53,9 +53,9 @@ function ErrorBoundaryError(props: Readonly<ErrorBoundaryErrorProps>) {
{error ? <div>{error.message}</div> : null}
{detailedError ? (
detailedError.map((d, index) => {
detailedError.map((d) => {
return (
<div key={index}>
<div key={`${d.fileName}:${d.lineNumber}:${d.columnNumber}`}>
{` at ${d.functionName} (${d.fileName}:${d.lineNumber}:${d.columnNumber})`}
</div>
);

View file

@ -58,8 +58,7 @@ interface AutoSuggestInputProps<T> extends Omit<
onChange?: (change: InputChanged<T>) => unknown;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function AutoSuggestInput<T = any>(props: AutoSuggestInputProps<T>) {
function AutoSuggestInput<T = unknown>(props: AutoSuggestInputProps<T>) {
const {
// TODO: forwaredRef should be replaces with React.forwardRef
forwardedRef,

View file

@ -21,17 +21,17 @@ function Form({
<div id={id}>
{validationErrors.length || validationWarnings.length ? (
<div className={styles.validationFailures}>
{validationErrors.map((error, index) => {
{validationErrors.map((error) => {
return (
<Alert key={index} kind={kinds.DANGER}>
<Alert key={error.propertyName} kind={kinds.DANGER}>
{error.errorMessage}
</Alert>
);
})}
{validationWarnings.map((warning, index) => {
{validationWarnings.map((warning) => {
return (
<Alert key={index} kind={kinds.WARNING}>
<Alert key={warning.propertyName} kind={kinds.WARNING}>
{warning.errorMessage}
</Alert>
);

View file

@ -269,10 +269,10 @@ function FormInputGroup<T, C extends InputType>(
{!checkInput && helpTexts ? (
<div>
{helpTexts.map((text, index) => {
{helpTexts.map((text) => {
return (
<FormInputHelpText
key={index}
key={text}
text={text}
isCheckInput={checkInput}
/>
@ -287,10 +287,12 @@ function FormInputGroup<T, C extends InputType>(
{helpLink ? <Link to={helpLink}>{translate('MoreInfo')}</Link> : null}
{errors.map((error, index) => {
{errors.map((error) => {
const message =
'errorMessage' in error ? error.errorMessage : error.message;
return 'errorMessage' in error ? (
<FormInputHelpText
key={index}
key={message}
text={error.errorMessage}
link={error.infoLink}
tooltip={error.detailedDescription}
@ -299,7 +301,7 @@ function FormInputGroup<T, C extends InputType>(
/>
) : (
<FormInputHelpText
key={index}
key={message}
text={error.message}
isError={true}
isCheckInput={checkInput}
@ -307,10 +309,12 @@ function FormInputGroup<T, C extends InputType>(
);
})}
{warnings.map((warning, index) => {
{warnings.map((warning) => {
const message =
'errorMessage' in warning ? warning.errorMessage : warning.message;
return 'errorMessage' in warning ? (
<FormInputHelpText
key={index}
key={message}
text={warning.errorMessage}
link={warning.infoLink}
tooltip={warning.detailedDescription}
@ -319,7 +323,7 @@ function FormInputGroup<T, C extends InputType>(
/>
) : (
<FormInputHelpText
key={index}
key={message}
text={warning.message}
isWarning={true}
isCheckInput={checkInput}

View file

@ -84,7 +84,7 @@ function KeyValueListInput({
>
{[...value, { key: '', value: '' }].map((v, index) => (
<KeyValueListInputItem
key={index}
key={`${index}-${v.key}-${v.value}`}
index={index}
keyValue={v.key}
value={v.value}

View file

@ -1,4 +1,5 @@
import classNames from 'classnames';
import { Data, ModifierFn } from 'popper.js';
import React, {
ElementType,
KeyboardEvent,
@ -189,11 +190,10 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
return '';
}, [value, values, isMultiSelect]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleComputeMaxHeight = useCallback((data: any) => {
const handleComputeMaxHeight: ModifierFn = useCallback((data: Data) => {
const windowHeight = window.innerHeight;
data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
data.styles.maxHeight = `${windowHeight - MINIMUM_DISTANCE_FROM_EDGE}px`;
return data;
}, []);

View file

@ -1,5 +1,5 @@
import classNames from 'classnames';
import React from 'react';
import React, { useMemo } from 'react';
import styles from './LoadingIndicator.css';
interface LoadingIndicatorProps {
@ -14,20 +14,24 @@ function LoadingIndicator({
size = 50,
}: Readonly<LoadingIndicatorProps>) {
const sizeInPx = `${size}px`;
const width = sizeInPx;
const height = sizeInPx;
const containerStyle = useMemo(() => ({ height: sizeInPx }), [sizeInPx]);
const rippleContainerStyle = useMemo(
() => ({ width: sizeInPx, height: sizeInPx }),
[sizeInPx]
);
return (
<div className={className} style={{ height }}>
<div className={className} style={containerStyle}>
<div
className={classNames(styles.rippleContainer, 'followingBalls')}
style={{ width, height }}
style={rippleContainerStyle}
>
<div className={rippleClassName} style={{ width, height }} />
<div className={rippleClassName} style={rippleContainerStyle} />
<div className={rippleClassName} style={{ width, height }} />
<div className={rippleClassName} style={rippleContainerStyle} />
<div className={rippleClassName} style={{ width, height }} />
<div className={rippleClassName} style={rippleContainerStyle} />
</div>
</div>
);

View file

@ -1,5 +1,5 @@
import classNames from 'classnames';
import React from 'react';
import React, { useMemo } from 'react';
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
import { Kind } from 'Helpers/Props/kinds';
import { Size } from 'Helpers/Props/sizes';
@ -35,6 +35,12 @@ function ProgressBar({
const progressText = text || progressPercent;
const actualWidth = width ? `${width}px` : '100%';
const widthStyle = useMemo(() => ({ width: actualWidth }), [actualWidth]);
const progressStyle = useMemo(
() => ({ width: progressPercent }),
[progressPercent]
);
return (
<ColorImpairedConsumer>
{(enableColorImpairedMode) => {
@ -42,12 +48,12 @@ function ProgressBar({
<div
className={classNames(containerClassName, styles[size])}
title={title}
style={{ width: actualWidth }}
style={widthStyle}
>
{showText && width ? (
<div
className={classNames(styles.backTextContainer, styles[kind])}
style={{ width: actualWidth }}
style={widthStyle}
>
<div className={styles.backText}>
<div>{progressText}</div>
@ -68,18 +74,15 @@ function ProgressBar({
aria-valuenow={Math.floor(progress)}
aria-valuemin={0}
aria-valuemax={100}
style={{ width: progressPercent }}
style={progressStyle}
/>
{showText ? (
<div
className={classNames(styles.frontTextContainer, styles[kind])}
style={{ width: progressPercent }}
style={progressStyle}
>
<div
className={styles.frontText}
style={{ width: actualWidth }}
>
<div className={styles.frontText} style={widthStyle}>
<div>{progressText}</div>
</div>
</div>

View file

@ -376,8 +376,8 @@ function InteractiveImportRow(props: Readonly<InteractiveImportRowProps>) {
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection.reason}</li>;
{rejections.map((rejection) => {
return <li key={rejection.reason}>{rejection.reason}</li>;
})}
</ul>
}

View file

@ -302,8 +302,8 @@ function InteractiveSearchRow(props: Readonly<InteractiveSearchRowProps>) {
title={translate('IndexerFlags')}
body={
<ul>
{indexerFlags.map((flag, index) => {
return <li key={index}>{flag}</li>;
{indexerFlags.map((flag) => {
return <li key={flag}>{flag}</li>;
})}
</ul>
}
@ -319,8 +319,8 @@ function InteractiveSearchRow(props: Readonly<InteractiveSearchRowProps>) {
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection}</li>;
{rejections.map((rejection) => {
return <li key={rejection}>{rejection}</li>;
})}
</ul>
}

View file

@ -16,8 +16,8 @@ function IndexerFlags({ indexerFlags = 0 }: Readonly<IndexerFlagsProps>) {
return flags.length ? (
<ul>
{flags.map((flag, index) => {
return <li key={index}>{flag.name}</li>;
{flags.map((flag) => {
return <li key={flag.id}>{flag.name}</li>;
})}
</ul>
) : null;

View file

@ -38,9 +38,9 @@ function ExtraFileDetailsPopover(
title={translate('Tags')}
body={
<ul>
{details.map(({ name, value }, index) => {
{details.map(({ name, value }) => {
return (
<li key={index}>
<li key={name}>
{name}: {value}
</li>
);

View file

@ -41,8 +41,12 @@ function AddRootFolder() {
<ul>
{Array.isArray(saveError.responseJSON) ? (
saveError.responseJSON.map((e, index) => {
return <li key={index}>{e.errorMessage}</li>;
saveError.responseJSON.map((e) => {
return (
<li key={e.propertyName ?? e.errorMessage}>
{e.errorMessage}
</li>
);
})
) : (
<li>{JSON.stringify(saveError.responseJSON)}</li>

View file

@ -83,7 +83,7 @@ export default function AutoTagging({
<TagList tags={tags} tagList={tagList} />
<div>
{specifications.map((item, index) => {
{specifications.map((item) => {
if (!item) {
return null;
}
@ -98,7 +98,7 @@ export default function AutoTagging({
}
return (
<Label key={index} kind={kind}>
<Label key={item.id} kind={kind}>
{item.name}
</Label>
);

View file

@ -54,10 +54,10 @@ export default function AddSpecificationItem({
</Button>
<MenuContent>
{presets.map((preset, index) => {
{presets.map((preset) => {
return (
<AddSpecificationPresetMenuItem
key={index}
key={preset.id}
name={preset.name}
implementation={implementation}
onPress={handleSpecificationSelect}

View file

@ -20,7 +20,7 @@ function UpdateChanges(props: Readonly<UpdateChangesProps>) {
<div>
<div className={styles.title}>{title}</div>
<ul>
{uniqueChanges.map((change, index) => {
{uniqueChanges.map((change) => {
const checkChange = change.replace(
/#\d{4,5}\b/g,
(match) =>
@ -30,7 +30,7 @@ function UpdateChanges(props: Readonly<UpdateChangesProps>) {
);
return (
<li key={index}>
<li key={change}>
<InlineMarkdown data={checkChange} />
</li>
);