From a063abaaafa0611cb03c658d3883a1af060ca885 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 4 Aug 2026 19:36:36 +0530 Subject: [PATCH 01/11] 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. --- .../issues/issue-layouts/properties/all-properties.tsx | 2 +- .../issues/issue-layouts/spreadsheet/columns/label-column.tsx | 2 +- apps/web/core/components/issues/peek-overview/properties.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx index 96def8e7c47..2efb1f8a4ac 100644 --- a/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/apps/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -179,7 +179,7 @@ export const IssueProperties = observer(function IssueProperties(props: IIssuePr issue.start_date && issue.target_date && displayProperties.start_date && displayProperties.due_date ); - const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]).filter(Boolean) || []; const minDate = getDate(issue.start_date); const maxDate = getDate(issue.target_date); diff --git a/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx b/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx index 02ae8b669fc..e89a4d0ff28 100644 --- a/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx +++ b/apps/web/core/components/issues/issue-layouts/spreadsheet/columns/label-column.tsx @@ -25,7 +25,7 @@ export const SpreadsheetLabelColumn = observer(function SpreadsheetLabelColumn(p // hooks const { labelMap } = useLabel(); - const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]).filter(Boolean) || []; return (
diff --git a/apps/web/core/components/issues/peek-overview/properties.tsx b/apps/web/core/components/issues/peek-overview/properties.tsx index bf280c8088d..8f35cc1645a 100644 --- a/apps/web/core/components/issues/peek-overview/properties.tsx +++ b/apps/web/core/components/issues/peek-overview/properties.tsx @@ -131,10 +131,10 @@ export const PeekOverviewProperties = observer(function PeekOverviewProperties(p > - {createdByDetails?.display_name.includes("-intake") ? "Plane" : createdByDetails?.display_name} + {createdByDetails?.display_name?.includes("-intake") ? "Plane" : createdByDetails?.display_name} )} From 508b0a523ab5f9fe9221ed7a33a9d8a219a6128c Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 4 Aug 2026 20:06:14 +0530 Subject: [PATCH 02/11] 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. --- .../core/components/core/description-versions/root.tsx | 2 +- apps/web/core/components/exporter/prev-exports.tsx | 2 +- .../components/integration/single-integration-card.tsx | 8 ++++++-- apps/web/core/components/profile/overview/activity.tsx | 2 +- .../core/store/issue/issue-details/sub_issues.store.ts | 4 ++-- 5 files changed, 11 insertions(+), 7 deletions(-) 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..b2b008a209f 100644 --- a/apps/web/core/components/exporter/prev-exports.tsx +++ b/apps/web/core/components/exporter/prev-exports.tsx @@ -53,7 +53,7 @@ export const PrevExports = observer(function PrevExports(props: Props) { 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); diff --git a/apps/web/core/components/integration/single-integration-card.tsx b/apps/web/core/components/integration/single-integration-card.tsx index c6773bb92df..ac36de03908 100644 --- a/apps/web/core/components/integration/single-integration-card.tsx +++ b/apps/web/core/components/integration/single-integration-card.tsx @@ -73,7 +73,9 @@ export const SingleIntegrationCard = observer(function SingleIntegrationCard({ i const handleRemoveIntegration = async () => { if (!workspaceSlug || !integration || !workspaceIntegrations) return; - const workspaceIntegrationId = workspaceIntegrations?.find((i) => i.integration === integration.id)?.id; + const workspaceIntegrationId = Array.isArray(workspaceIntegrations) + ? workspaceIntegrations.find((i) => i.integration === integration.id)?.id + : undefined; setDeletingIntegration(true); @@ -104,7 +106,9 @@ export const SingleIntegrationCard = observer(function SingleIntegrationCard({ i }); }; - const isInstalled = workspaceIntegrations?.find((i: any) => i.integration_detail.id === integration.id); + const isInstalled = Array.isArray(workspaceIntegrations) + ? workspaceIntegrations.find((i: any) => i.integration_detail.id === integration.id) + : undefined; return (
diff --git a/apps/web/core/components/profile/overview/activity.tsx b/apps/web/core/components/profile/overview/activity.tsx index 0b089ba5ca5..93488e6c9a8 100644 --- a/apps/web/core/components/profile/overview/activity.tsx +++ b/apps/web/core/components/profile/overview/activity.tsx @@ -45,7 +45,7 @@ export const ProfileActivity = observer(function ProfileActivity() {

{t("profile.stats.recent_activity.title")}

- {userProfileActivity ? ( + {Array.isArray(userProfileActivity?.results) ? ( userProfileActivity.results.length > 0 ? (
{userProfileActivity.results.map((activity) => ( diff --git a/apps/web/core/store/issue/issue-details/sub_issues.store.ts b/apps/web/core/store/issue/issue-details/sub_issues.store.ts index 7410ce30369..2832cf343ec 100644 --- a/apps/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/apps/web/core/store/issue/issue-details/sub_issues.store.ts @@ -166,8 +166,8 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { sub_issue_ids: issueIds, }); - const subIssuesStateDistribution = response?.state_distribution; - const subIssues = response.sub_issues as TIssue[]; + const subIssuesStateDistribution = response?.state_distribution ?? {}; + const subIssues = (response.sub_issues ?? []) as TIssue[]; // fetch other issues states and members when sub-issues are from different project if (subIssues && subIssues.length > 0) { From c845542b0dc1949ba026601430b5dedef2fe24bc Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 4 Aug 2026 20:11:34 +0530 Subject: [PATCH 03/11] 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. --- .../common/layout-error-boundary.tsx | 48 +++++++++++++++++++ .../issues/issue-layouts/issue-layout-HOC.tsx | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 apps/web/core/components/common/layout-error-boundary.tsx diff --git a/apps/web/core/components/common/layout-error-boundary.tsx b/apps/web/core/components/common/layout-error-boundary.tsx new file mode 100644 index 00000000000..8db4df5f0f1 --- /dev/null +++ b/apps/web/core/components/common/layout-error-boundary.tsx @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { Component } from "react"; +import type { ErrorInfo, ReactNode } from "react"; +import { AlertTriangle } from "lucide-react"; +import { Button } from "@plane/propel/button"; + +type Props = { + children: ReactNode; +}; + +type State = { + hasError: boolean; +}; + +// 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 }; + + static getDerivedStateFromError(): State { + return { hasError: true }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + // eslint-disable-next-line no-console + console.error("Issue layout crashed", error, info); + } + + render() { + if (this.state.hasError) { + return ( +
+ +

Something went wrong while loading this view.

+ +
+ ); + } + return this.props.children; + } +} diff --git a/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx b/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx index 50a0323a9af..2c6cd65f494 100644 --- a/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx +++ b/apps/web/core/components/issues/issue-layouts/issue-layout-HOC.tsx @@ -8,6 +8,7 @@ import { observer } from "mobx-react"; // plane imports import { EIssueLayoutTypes } from "@plane/types"; // components +import { LayoutErrorBoundary } from "@/components/common/layout-error-boundary"; import { CalendarLayoutLoader } from "@/components/ui/loader/layouts/calendar-layout-loader"; import { GanttLayoutLoader } from "@/components/ui/loader/layouts/gantt-layout-loader"; import { KanbanLayoutLoader } from "@/components/ui/loader/layouts/kanban-layout-loader"; @@ -58,5 +59,5 @@ export const IssueLayoutHOC = observer(function IssueLayoutHOC(props: Props) { return ; } - return <>{props.children}; + return {props.children}; }); From c46109c0f5a0dfc64edd4c5e9f28977b7f881899 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 4 Aug 2026 20:17:05 +0530 Subject: [PATCH 04/11] 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. --- apps/web/core/components/exporter/prev-exports.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/web/core/components/exporter/prev-exports.tsx b/apps/web/core/components/exporter/prev-exports.tsx index b2b008a209f..17b09821800 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,17 @@ export const PrevExports = observer(function PrevExports(props: Props) { workspaceSlug && cursor ? () => integrationService.getExportsServicesList(workspaceSlug, cursor, per_page) : null ); - const handleRefresh = () => { + const handleRefresh = useCallback(() => { setRefreshing(true); mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`)).then(() => setRefreshing(false)); - }; + }, [workspaceSlug, cursor, per_page]); useEffect(() => { const interval = setInterval(() => { - if (Array.isArray(exporterServices?.results) && 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 +64,7 @@ export const PrevExports = observer(function PrevExports(props: Props) { }, 3000); return () => clearInterval(interval); - }, [exporterServices]); + }, [exporterServices, handleRefresh]); return (
From 63bd51fd9994d2432b278535251e178a98c09be8 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 4 Aug 2026 20:24:18 +0530 Subject: [PATCH 05/11] 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. --- .../common/layout-error-boundary.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/apps/web/core/components/common/layout-error-boundary.tsx b/apps/web/core/components/common/layout-error-boundary.tsx index 8db4df5f0f1..74ef8f3fbb8 100644 --- a/apps/web/core/components/common/layout-error-boundary.tsx +++ b/apps/web/core/components/common/layout-error-boundary.tsx @@ -4,9 +4,10 @@ * See the LICENSE file for details. */ -import { Component } from "react"; +import { Component, Fragment } from "react"; import type { ErrorInfo, ReactNode } from "react"; import { AlertTriangle } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; type Props = { @@ -15,14 +16,29 @@ type Props = { type State = { hasError: boolean; + retryKey: number; }; +function LayoutErrorFallback({ onRetry }: { onRetry: () => 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 }; + state: State = { hasError: false, retryKey: 0 }; - static getDerivedStateFromError(): State { + static getDerivedStateFromError(): Partial { return { hasError: true }; } @@ -31,18 +47,14 @@ export class LayoutErrorBoundary extends Component { console.error("Issue layout crashed", error, info); } + handleRetry = () => { + this.setState((prev) => ({ hasError: false, retryKey: prev.retryKey + 1 })); + }; + render() { if (this.state.hasError) { - return ( -
- -

Something went wrong while loading this view.

- -
- ); + return ; } - return this.props.children; + return {this.props.children}; } } From 77838ae16a027c5a00179ff170002ad9e5632c85 Mon Sep 17 00:00:00 2001 From: Atul Tameshwari Date: Tue, 4 Aug 2026 20:25:04 +0530 Subject: [PATCH 06/11] 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. --- .../core/components/common/activity/user.tsx | 2 +- .../core/components/exporter/prev-exports.tsx | 52 +++++++++---------- .../properties/all-properties.tsx | 5 +- .../spreadsheet/columns/label-column.tsx | 5 +- .../draft-issue-properties.tsx | 5 +- .../issue/issue-details/sub_issues.store.ts | 2 +- 6 files changed, 39 insertions(+), 32 deletions(-) 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"} ) : (
- {!!exporterServices?.results?.length && ( + {Array.isArray(exporterServices?.results) && exporterServices.results.length > 0 && (