Skip to content

Refactored several React class components as functional components #8750

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 15, 2025
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
30 changes: 14 additions & 16 deletions frontend/javascripts/components/disable_generic_dnd.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import window from "libs/window";
import React from "react";
import type { EmptyObject } from "types/globals";
export default class DisableGenericDnd extends React.Component<EmptyObject> {
componentDidMount() {
window.addEventListener("dragover", this.preventDefault, false);
window.addEventListener("drop", this.preventDefault, false);
}

componentWillUnmount() {
window.removeEventListener("dragover", this.preventDefault);
window.removeEventListener("drop", this.preventDefault);
}
const preventDefault = (e: Event) => {
e.preventDefault();
};

preventDefault = (e: Event) => {
e.preventDefault();
};
export default function DisableGenericDnd() {
React.useEffect(() => {
window.addEventListener("dragover", preventDefault, false);
window.addEventListener("drop", preventDefault, false);

render() {
return null;
}
return () => {
window.removeEventListener("dragover", preventDefault);
window.removeEventListener("drop", preventDefault);
};
}, []);

return null;
}
136 changes: 67 additions & 69 deletions frontend/javascripts/components/fixed_expandable_table.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { Button, Table, type TableProps } from "antd";
import type { ColumnsType, GetRowKey } from "antd/lib/table/interface";
import React from "react";
import type { ColumnsType, ExpandableConfig, GetRowKey } from "antd/lib/table/interface";
import type React from "react";
import { useEffect, useState } from "react";

type State = {
expandedRows: Array<string>;
className?: string;
};
/** This is a wrapper for large tables that have fixed columns and support expanded rows.
* This wrapper ensures that when rows are expanded no column is fixed as this creates rendering bugs.
* If you are using this wrapper, you do not need to set the class "large-table"
Expand All @@ -17,72 +14,73 @@ type OwnTableProps<RecordType = any> = TableProps<RecordType> & {
columns: ColumnsType<RecordType>;
};

const EMPTY_ARRAY: string[] = [] as const;
const EMPTY_ARRAY: React.Key[] = [] as const;

export default class FixedExpandableTable extends React.PureComponent<OwnTableProps, State> {
state: State = {
expandedRows: EMPTY_ARRAY,
};
const getAllRowIds = (
dataSource: readonly any[] | undefined,
rowKey: string | number | symbol | GetRowKey<any> | undefined,
) => {
const canUseRowKey = typeof rowKey === "string";
return dataSource != null && canUseRowKey ? dataSource.map((row) => row[rowKey]) : [];
};

export default function FixedExpandableTable<RecordType>({
className,
expandable,
dataSource,
rowKey,
columns,
...restProps
}: OwnTableProps<RecordType>) {
const [expandedRows, setExpandedRows] = useState<React.Key[]>(EMPTY_ARRAY);

getAllRowIds(
dataSource: readonly any[] | undefined,
rowKey: string | number | symbol | GetRowKey<any> | undefined,
) {
const canUseRowKey = typeof rowKey === "string";
return dataSource != null && canUseRowKey ? dataSource.map((row) => row[rowKey]) : [];
}
// biome-ignore lint/correctness/useExhaustiveDependencies: Collapse all rows when source changes
useEffect(() => {
setExpandedRows(EMPTY_ARRAY);
}, [dataSource]);

componentDidUpdate(prevProps: Readonly<TableProps<any>>): void {
if (prevProps.dataSource !== this.props.dataSource) {
this.setState({ expandedRows: EMPTY_ARRAY });
}
}
const areAllRowsExpanded = dataSource != null && expandedRows.length === dataSource?.length;

render() {
const { expandedRows } = this.state;
const { className, expandable, ...restProps } = this.props;
const { dataSource, rowKey } = this.props;
const areAllRowsExpanded =
dataSource != null && this.state.expandedRows.length === dataSource?.length;
const columnTitleCollapsed = (
<Button
className="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
title="Expand all rows"
onClick={() => setExpandedRows(getAllRowIds(dataSource, rowKey))}
/>
);
const columnTitleExpanded = (
<Button
className="ant-table-row-expand-icon ant-table-row-expand-icon-expanded"
title="Collapse all rows"
onClick={() => setExpandedRows(EMPTY_ARRAY)}
/>
);

const columnsWithAdjustedFixedProp = columns.map((column) => {
const columnFixed = expandedRows.length > 0 ? false : column.fixed;
return { ...column, fixed: columnFixed };
});

const expandableProp: ExpandableConfig<RecordType> = {
...expandable,
expandedRowKeys: expandedRows,
onExpandedRowsChange: (selectedRows: readonly React.Key[]) => {
setExpandedRows(selectedRows as React.Key[]);
},
columnTitle: areAllRowsExpanded ? columnTitleExpanded : columnTitleCollapsed,
};

const columnTitleCollapsed = (
<Button
className="ant-table-row-expand-icon ant-table-row-expand-icon-collapsed"
title="Expand all rows"
onClick={() => this.setState({ expandedRows: this.getAllRowIds(dataSource, rowKey) })}
/>
);
const columnTitleExpanded = (
<Button
className="ant-table-row-expand-icon ant-table-row-expand-icon-expanded"
title="Collapse all rows"
onClick={() => this.setState({ expandedRows: EMPTY_ARRAY })}
/>
);
const columnsWithAdjustedFixedProp: TableProps["columns"] = this.props.columns.map((column) => {
const columnFixed = expandedRows.length > 0 ? false : column.fixed;
return { ...column, fixed: columnFixed };
});
const expandableProp = {
...expandable,
expandedRowKeys: expandedRows,
onExpandedRowsChange: (selectedRows: readonly React.Key[]) => {
this.setState({
expandedRows: selectedRows as string[],
});
},
columnTitle: areAllRowsExpanded ? columnTitleExpanded : columnTitleCollapsed,
};
return (
<Table
{...restProps}
expandable={expandableProp}
scroll={{
x: "max-content",
}}
className={`large-table ${className}`}
columns={columnsWithAdjustedFixedProp}
/>
);
}
return (
<Table
{...restProps}
dataSource={dataSource}
rowKey={rowKey}
expandable={expandableProp}
scroll={{
x: "max-content",
}}
className={`large-table ${className}`}
columns={columnsWithAdjustedFixedProp}
/>
);
}
41 changes: 18 additions & 23 deletions frontend/javascripts/components/highlightable_row.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
import * as React from "react";
import type React from "react";
import { useState } from "react";

type Props = {
shouldHighlight: boolean;
children: React.ReactNode;
style?: Record<string, any>;
};
type State = {
persistedShouldHighlight: boolean;
}; // This component is able to highlight a newly rendered row.

// This component is able to highlight a newly rendered row.
// Internally, it persists the initially passed props, since that
// prop can change faster than the animation is executed. Not saving
// the initial prop, would abort the animation too early.
export default function HighlightableRow({ shouldHighlight, style, ...restProps }: Props) {
const [persistedShouldHighlight] = useState(shouldHighlight);

export default class HighlightableRow extends React.PureComponent<Props, State> {
state: State = {
persistedShouldHighlight: this.props.shouldHighlight,
};

render() {
const { shouldHighlight, style, ...restProps } = this.props;
return (
<tr
{...restProps}
style={{
...style,
animation: this.state.persistedShouldHighlight ? "highlight-background 2.0s ease" : "",
}}
>
{this.props.children}
</tr>
);
}
return (
<tr
{...restProps}
style={{
...style,
animation: persistedShouldHighlight ? "highlight-background 2.0s ease" : "",
}}
>
{restProps.children}
</tr>
);
}
29 changes: 11 additions & 18 deletions frontend/javascripts/components/loop.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,21 @@
import window from "libs/window";
import { Component } from "react";
import type { EmptyObject } from "types/globals";
import { useCallback, useEffect } from "react";

type LoopProps = {
interval: number;
onTick: (...args: Array<any>) => any;
};

class Loop extends Component<LoopProps, EmptyObject> {
intervalId: number | null | undefined = null;
export default function Loop({ interval, onTick }: LoopProps) {
const _onTick = useCallback(onTick, []);

componentDidMount() {
this.intervalId = window.setInterval(this.props.onTick, this.props.interval);
}
useEffect(() => {
const intervalId = window.setInterval(_onTick, interval);

componentWillUnmount() {
if (this.intervalId != null) {
window.clearInterval(this.intervalId);
this.intervalId = null;
}
}
return () => {
window.clearInterval(intervalId);
};
}, [interval, _onTick]);

render() {
return null;
}
return null;
}

export default Loop;
Loading