From fa027167f62638166dd1b43dff0c9bd2e559aa7b Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Wed, 5 Aug 2026 00:21:19 +0530 Subject: [PATCH 1/2] fix(web): guard unguarded data derefs causing work-item and layout crashes (#9546) * fix: filter out undefined label options in issue properties components Updated the mapping of label IDs to ensure that only defined label options are included in the defaultLabelOptions array across multiple components. This change enhances the robustness of the label handling in the IssueProperties and SpreadsheetLabelColumn components, as well as in the PeekOverviewProperties component. * fix: ensure array checks for results in various components Updated multiple components to include checks for array types before accessing results. This change enhances stability by preventing potential runtime errors when results are undefined or not an array. Affected components include DescriptionVersionsRoot, PrevExports, SingleIntegrationCard, ProfileActivity, and IssueSubIssuesStore. * fix: wrap children in LayoutErrorBoundary for improved error handling Updated the IssueLayoutHOC component to include LayoutErrorBoundary, enhancing error handling by wrapping the children. This change aims to provide a more robust user experience by catching layout-related errors effectively. * fix: optimize handleRefresh with useCallback in PrevExports component Refactored the handleRefresh function in the PrevExports component to use useCallback, improving performance by memoizing the function. Additionally, updated the useEffect dependency array to include handleRefresh, ensuring the effect runs correctly when dependencies change. This change enhances the efficiency of the component's refresh logic. * fix: enhance LayoutErrorBoundary with retry functionality and improved error messaging Refactored the LayoutErrorBoundary component to include a dedicated LayoutErrorFallback for better error presentation. Added a retry mechanism that allows users to attempt to reload the content after an error occurs. This change improves user experience by providing clearer messaging and a more interactive way to recover from errors. * fix: improve label option handling and array checks in various components Refactored the defaultLabelOptions logic in multiple components to use flatMap for better handling of undefined labels. Additionally, updated array checks in the PrevExports component to ensure results are properly validated before access. These changes enhance the robustness and stability of the components, preventing potential runtime errors. * fix: refactor ProfileActivity component for improved loading and data handling Updated the ProfileActivity component to enhance the loading state management and streamline the rendering of user activity results. The refactor includes a more efficient check for userProfileActivity, ensuring that loading indicators and empty states are displayed correctly. This change improves the user experience by providing clearer feedback during data fetching and handling scenarios with no activity results. * fix: improve type safety and array handling in integration card and sub-issues store Updated the SingleIntegrationCard component to use a specific type for workspace integrations, enhancing type safety. Additionally, refactored the subIssues assignment in the IssueSubIssuesStore to ensure it correctly checks for an array before assignment, improving stability and preventing potential runtime errors. * fix: enhance handleRefresh in PrevExports component with error handling Refactored the handleRefresh function in the PrevExports component to include error handling during the refresh process. The function now uses async/await for better readability and ensures that any errors during the mutation are logged, improving the robustness of the component's refresh logic. * style: fix oxfmt formatting flagged by CI check:format Multi-line flatMap guard needed reformatting to satisfy oxfmt. * style: reformat defaultLabelOptions logic for consistency Adjusted the formatting of the defaultLabelOptions logic in the DraftIssueProperties component to maintain consistency with the project's coding standards. This change enhances readability without altering functionality. --- .../core/components/common/activity/user.tsx | 2 +- .../common/layout-error-boundary.tsx | 60 +++++++++++++++ .../core/description-versions/root.tsx | 2 +- .../core/components/exporter/prev-exports.tsx | 74 ++++++++++--------- .../integration/single-integration-card.tsx | 8 +- .../issues/issue-layouts/issue-layout-HOC.tsx | 3 +- .../properties/all-properties.tsx | 6 +- .../spreadsheet/columns/label-column.tsx | 6 +- .../issues/peek-overview/properties.tsx | 4 +- .../draft-issue-properties.tsx | 6 +- .../components/profile/overview/activity.tsx | 66 ++++++++--------- .../issue/issue-details/sub_issues.store.ts | 4 +- 12 files changed, 161 insertions(+), 80 deletions(-) create mode 100644 apps/web/core/components/common/layout-error-boundary.tsx diff --git a/apps/web/core/components/common/activity/user.tsx b/apps/web/core/components/common/activity/user.tsx index 1ba3d03b750..595a960fb48 100644 --- a/apps/web/core/components/common/activity/user.tsx +++ b/apps/web/core/components/common/activity/user.tsx @@ -28,7 +28,7 @@ export const User = observer(function User(props: TUser) { return ( <> - {customUserName || actorDetail?.display_name.includes("-intake") ? ( + {customUserName || actorDetail?.display_name?.includes("-intake") ? ( {customUserName || "Plane"} ) : ( void }) { + const { t } = useTranslation(); + + return ( +
+ +

{t("something_went_wrong")}

+ +
+ ); +} + +// Catches render crashes from a single issue layout (list/kanban/spreadsheet/calendar/gantt) +// so a bad group/column shape degrades to a local fallback instead of taking down the whole page. +export class LayoutErrorBoundary extends Component { + state: State = { hasError: false, retryKey: 0 }; + + static getDerivedStateFromError(): Partial { + return { hasError: true }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + // eslint-disable-next-line no-console + console.error("Issue layout crashed", error, info); + } + + handleRetry = () => { + this.setState((prev) => ({ hasError: false, retryKey: prev.retryKey + 1 })); + }; + + render() { + if (this.state.hasError) { + return ; + } + return {this.props.children}; + } +} diff --git a/apps/web/core/components/core/description-versions/root.tsx b/apps/web/core/components/core/description-versions/root.tsx index 9922cd27ac1..0f1a577b2a0 100644 --- a/apps/web/core/components/core/description-versions/root.tsx +++ b/apps/web/core/components/core/description-versions/root.tsx @@ -50,7 +50,7 @@ export const DescriptionVersionsRoot = observer(function DescriptionVersionsRoot entityId && activeVersionId ? `DESCRIPTION_VERSION_DETAILS_${activeVersionId}` : null, entityId && activeVersionId ? () => fetchHandlers.retrieveDescriptionVersion(entityId, activeVersionId) : null ); - const versions = versionsListResponse?.results; + const versions = Array.isArray(versionsListResponse?.results) ? versionsListResponse.results : undefined; const versionsCount = versions?.length ?? 0; const activeVersionDetails = versions?.find((version) => version.id === activeVersionId); const activeVersionIndex = versions?.findIndex((version) => version.id === activeVersionId); diff --git a/apps/web/core/components/exporter/prev-exports.tsx b/apps/web/core/components/exporter/prev-exports.tsx index 2334a6c159c..f3ebedb41f0 100644 --- a/apps/web/core/components/exporter/prev-exports.tsx +++ b/apps/web/core/components/exporter/prev-exports.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { observer } from "mobx-react"; import useSWR, { mutate } from "swr"; import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; @@ -46,14 +46,24 @@ export const PrevExports = observer(function PrevExports(props: Props) { workspaceSlug && cursor ? () => integrationService.getExportsServicesList(workspaceSlug, cursor, per_page) : null ); - const handleRefresh = () => { + const handleRefresh = useCallback(async () => { setRefreshing(true); - mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`)).then(() => setRefreshing(false)); - }; + try { + await mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`)); + } catch (error) { + // eslint-disable-next-line no-console + console.error("Failed to refresh export services list", error); + } finally { + setRefreshing(false); + } + }, [workspaceSlug, cursor, per_page]); useEffect(() => { const interval = setInterval(() => { - if (exporterServices?.results?.some((service) => service.status === "processing")) { + if ( + Array.isArray(exporterServices?.results) && + exporterServices.results.some((service) => service.status === "processing") + ) { handleRefresh(); } else { clearInterval(interval); @@ -61,7 +71,7 @@ export const PrevExports = observer(function PrevExports(props: Props) { }, 3000); return () => clearInterval(interval); - }, [exporterServices]); + }, [exporterServices, handleRefresh]); return (
@@ -73,7 +83,7 @@ export const PrevExports = observer(function PrevExports(props: Props) { {refreshing ? t("refreshing") : t("refresh_status")}
- {!!exporterServices?.results?.length && ( + {Array.isArray(exporterServices?.results) && exporterServices.results.length > 0 && (