From 33417f51c8046e17a9b57aa87d0ce23e12674993 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 00:14:51 +0530 Subject: [PATCH 01/10] Add change request stats hook and page integration Introduce a new hook useGetProjectChangeRequestStats to fetch and map change request stats from the backend (with response type ChangeRequestStatsResponse). Add a new ApiQueryKeys entry (CHANGE_REQUEST_STATS). Integrate the hook into ChangeRequestsPage: import and call the hook, remove the previous mocked stats, use the hook's loading/error states for the stat cards, and ensure exports wait for stats to be available. Includes mapping logic to derive scheduled, inProgress, and completed counts from API state counts and basic error/log handling. --- .../api/useGetProjectChangeRequestStats.ts | 111 ++++++++++++++++++ .../webapp/src/constants/apiConstants.ts | 1 + .../webapp/src/models/responses.ts | 10 ++ .../webapp/src/pages/ChangeRequestsPage.tsx | 26 ++-- 4 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts diff --git a/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts b/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts new file mode 100644 index 0000000000..21a4478430 --- /dev/null +++ b/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts @@ -0,0 +1,111 @@ +// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import { useQuery, type UseQueryResult } from "@tanstack/react-query"; +import { useAsgardeo } from "@asgardeo/react"; +import { useLogger } from "@hooks/useLogger"; +import { ApiQueryKeys } from "@constants/apiConstants"; +import { useAuthApiClient } from "@context/AuthApiContext"; +import type { ChangeRequestStatsResponse, ChangeRequestStats } from "@models/responses"; + +/** + * Maps the API response to ChangeRequestStats. + * + * @param {ChangeRequestStatsResponse} response - The API response. + * @returns {ChangeRequestStats} The mapped stats object. + */ +function mapChangeRequestStats(response: ChangeRequestStatsResponse): ChangeRequestStats { + const { totalCount, stateCount } = response; + + // Find scheduled count (label: "Scheduled") + const scheduled = stateCount.find((state) => state.label === "Scheduled")?.count ?? 0; + + // Calculate in-progress count (sum of: Implement, Review, Customer Approval, Customer Review) + const inProgressLabels = ["Implement", "Review", "Customer Approval", "Customer Review"]; + const inProgress = stateCount + .filter((state) => inProgressLabels.includes(state.label)) + .reduce((sum, state) => sum + state.count, 0); + + // Calculate completed count (sum of: Closed, Canceled, Rollback) + const completedLabels = ["Closed", "Canceled", "Rollback"]; + const completed = stateCount + .filter((state) => completedLabels.includes(state.label)) + .reduce((sum, state) => sum + state.count, 0); + + return { + totalRequests: totalCount, + scheduled, + inProgress, + completed, + }; +} + +/** + * Custom hook to fetch project change request statistics. + * + * @param {string} projectId - The ID of the project. + * @returns {UseQueryResult} The query result object. + */ +export function useGetProjectChangeRequestStats( + projectId: string, +): UseQueryResult { + const logger = useLogger(); + const { isSignedIn, isLoading: isAuthLoading } = useAsgardeo(); + const fetchFn = useAuthApiClient(); + + return useQuery({ + queryKey: [ApiQueryKeys.CHANGE_REQUEST_STATS, projectId], + queryFn: async (): Promise => { + logger.debug(`[useGetProjectChangeRequestStats] Fetching change request stats for project ID: ${projectId}`); + + try { + const baseUrl = window.config?.CUSTOMER_PORTAL_BACKEND_BASE_URL; + + if (!baseUrl) { + throw new Error("CUSTOMER_PORTAL_BACKEND_BASE_URL is not configured"); + } + + const requestUrl = `${baseUrl}/projects/${projectId}/stats/change-requests`; + const response = await fetchFn(requestUrl, { method: "GET" }); + + logger.debug( + `[useGetProjectChangeRequestStats] Response status for ${projectId}: ${response.status}`, + ); + + if (!response.ok) { + throw new Error( + `Error fetching change request stats: ${response.statusText}`, + ); + } + + const data: ChangeRequestStatsResponse = await response.json(); + logger.debug("[useGetProjectChangeRequestStats] Data received:", data); + + // Map the API response to the expected stats format + const mappedStats = mapChangeRequestStats(data); + logger.debug("[useGetProjectChangeRequestStats] Mapped stats:", mappedStats); + + return mappedStats; + } catch (error) { + logger.error("[useGetProjectChangeRequestStats] Error:", error); + throw error; + } + }, + enabled: !!projectId && isSignedIn && !isAuthLoading, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); +} diff --git a/apps/customer-portal/webapp/src/constants/apiConstants.ts b/apps/customer-portal/webapp/src/constants/apiConstants.ts index 04b177e7e7..c5e0c19686 100644 --- a/apps/customer-portal/webapp/src/constants/apiConstants.ts +++ b/apps/customer-portal/webapp/src/constants/apiConstants.ts @@ -51,6 +51,7 @@ export const ApiQueryKeys = { CHANGE_REQUESTS: "change-requests", CHANGE_REQUEST_DETAILS: "change-request-details", CHANGE_REQUEST_COMMENTS: "change-request-comments", + CHANGE_REQUEST_STATS: "change-request-stats", } as const; // Constants for API-related mutation keys. diff --git a/apps/customer-portal/webapp/src/models/responses.ts b/apps/customer-portal/webapp/src/models/responses.ts index f01b3232e6..cd5a345e29 100644 --- a/apps/customer-portal/webapp/src/models/responses.ts +++ b/apps/customer-portal/webapp/src/models/responses.ts @@ -368,6 +368,16 @@ export interface ChangeRequestStats { completed: number; } +// Change Request Stats API Response +export interface ChangeRequestStatsResponse { + totalCount: number; + stateCount: Array<{ + id: string; + label: string; + count: number; + }>; +} + // Case Search Response export interface CaseSearchResponse { cases: CaseListItem[]; diff --git a/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx b/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx index e30c119612..138f76799d 100644 --- a/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx +++ b/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx @@ -45,6 +45,7 @@ import useGetProjectFilters from "@api/useGetProjectFilters"; import useGetChangeRequests, { useGetChangeRequestsInfinite, } from "@api/useGetChangeRequests"; +import { useGetProjectChangeRequestStats } from "@api/useGetProjectChangeRequestStats"; import ChangeRequestsStatCards from "@components/support/change-requests/ChangeRequestsStatCards"; import ChangeRequestsSearchBar from "@components/support/change-requests/ChangeRequestsSearchBar"; import ChangeRequestsList from "@components/support/change-requests/ChangeRequestsList"; @@ -72,6 +73,13 @@ export default function ChangeRequestsPage(): JSX.Element { // Fetch filter metadata (deployments etc.) const { data: filterMetadata } = useGetProjectFilters(projectId || ""); + // Fetch change request stats from API + const { + data: stats, + isLoading: isStatsLoading, + isError: isStatsError, + } = useGetProjectChangeRequestStats(projectId || ""); + // Build API request (following cases listing pattern) const changeRequestSearchRequest = useMemo< Omit @@ -157,17 +165,6 @@ export default function ChangeRequestsPage(): JSX.Element { : infiniteData?.pages[0]?.totalRecords || 0; const totalPages = Math.ceil(totalRecords / pageSize); - // Mock stats for now - TODO: implement stats API - const stats = useMemo( - () => ({ - totalRequests: totalRecords, - scheduled: 0, - inProgress: 0, - completed: 0, - }), - [totalRecords], - ); - // Handle export completion when all data is fetched useEffect(() => { if (isExporting) { @@ -179,7 +176,8 @@ export default function ChangeRequestsPage(): JSX.Element { !isInfiniteLoading && !hasNextPage && !isFetchingNextPage && - infiniteData + infiniteData && + stats ) { // Success case - all data fetched const allChangeRequests = @@ -293,8 +291,8 @@ export default function ChangeRequestsPage(): JSX.Element { {/* Stat cards */} From 8edc5daa6444fb8a1b85183ecfd2f6115b270f13 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 00:21:22 +0530 Subject: [PATCH 02/10] Format stats hook and defer export state updates Reformat useGetProjectChangeRequestStats.ts for readability (multi-line imports, function params, arrays, and logger calls) and clean up whitespace; no behavioral changes to the mapping logic. In ChangeRequestsPage.tsx, wrap setIsExporting(false) in setTimeout(..., 0) when handling export success/error to defer the state update and avoid synchronous React state updates during render or while unmounting. --- .../api/useGetProjectChangeRequestStats.ts | 34 ++++++++++++++----- .../webapp/src/pages/ChangeRequestsPage.tsx | 4 +-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts b/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts index 21a4478430..a8a544a282 100644 --- a/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts +++ b/apps/customer-portal/webapp/src/api/useGetProjectChangeRequestStats.ts @@ -19,22 +19,33 @@ import { useAsgardeo } from "@asgardeo/react"; import { useLogger } from "@hooks/useLogger"; import { ApiQueryKeys } from "@constants/apiConstants"; import { useAuthApiClient } from "@context/AuthApiContext"; -import type { ChangeRequestStatsResponse, ChangeRequestStats } from "@models/responses"; +import type { + ChangeRequestStatsResponse, + ChangeRequestStats, +} from "@models/responses"; /** * Maps the API response to ChangeRequestStats. - * + * * @param {ChangeRequestStatsResponse} response - The API response. * @returns {ChangeRequestStats} The mapped stats object. */ -function mapChangeRequestStats(response: ChangeRequestStatsResponse): ChangeRequestStats { +function mapChangeRequestStats( + response: ChangeRequestStatsResponse, +): ChangeRequestStats { const { totalCount, stateCount } = response; // Find scheduled count (label: "Scheduled") - const scheduled = stateCount.find((state) => state.label === "Scheduled")?.count ?? 0; + const scheduled = + stateCount.find((state) => state.label === "Scheduled")?.count ?? 0; // Calculate in-progress count (sum of: Implement, Review, Customer Approval, Customer Review) - const inProgressLabels = ["Implement", "Review", "Customer Approval", "Customer Review"]; + const inProgressLabels = [ + "Implement", + "Review", + "Customer Approval", + "Customer Review", + ]; const inProgress = stateCount .filter((state) => inProgressLabels.includes(state.label)) .reduce((sum, state) => sum + state.count, 0); @@ -69,7 +80,9 @@ export function useGetProjectChangeRequestStats( return useQuery({ queryKey: [ApiQueryKeys.CHANGE_REQUEST_STATS, projectId], queryFn: async (): Promise => { - logger.debug(`[useGetProjectChangeRequestStats] Fetching change request stats for project ID: ${projectId}`); + logger.debug( + `[useGetProjectChangeRequestStats] Fetching change request stats for project ID: ${projectId}`, + ); try { const baseUrl = window.config?.CUSTOMER_PORTAL_BACKEND_BASE_URL; @@ -93,11 +106,14 @@ export function useGetProjectChangeRequestStats( const data: ChangeRequestStatsResponse = await response.json(); logger.debug("[useGetProjectChangeRequestStats] Data received:", data); - + // Map the API response to the expected stats format const mappedStats = mapChangeRequestStats(data); - logger.debug("[useGetProjectChangeRequestStats] Mapped stats:", mappedStats); - + logger.debug( + "[useGetProjectChangeRequestStats] Mapped stats:", + mappedStats, + ); + return mappedStats; } catch (error) { logger.error("[useGetProjectChangeRequestStats] Error:", error); diff --git a/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx b/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx index 138f76799d..e4e712f6a8 100644 --- a/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx +++ b/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx @@ -171,7 +171,7 @@ export default function ChangeRequestsPage(): JSX.Element { if (isInfiniteError) { // Handle error case console.error("Failed to fetch change requests for export"); - setIsExporting(false); + setTimeout(() => setIsExporting(false), 0); } else if ( !isInfiniteLoading && !hasNextPage && @@ -186,7 +186,7 @@ export default function ChangeRequestsPage(): JSX.Element { page.changeRequests, ) || []; generateChangeRequestsSchedulePdf(allChangeRequests, stats); - setIsExporting(false); + setTimeout(() => setIsExporting(false), 0); } } }, [ From 9b9f5057db5275067ef617bd8b645c4d8ccd1098 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 00:28:32 +0530 Subject: [PATCH 03/10] Use deployments for deployment filter Wire project deployments into the all-cases filters so the deployment dropdown uses real deployment items instead of generic metadata. Added optional deployments props to AllCasesFilters and AllCasesSearchBar, imported the ProjectDeploymentItem type, and updated AllCasesFilters to build options from deployments (label = type.label || name, value = id) when the filter id is "deployment". AllCasesPage now fetches deployments via useGetDeployments and passes them through to the search bar; when deployments are not present, the component falls back to the existing metadata-based options. --- .../support/all-cases/AllCasesFilters.tsx | 30 +++++++++++++------ .../support/all-cases/AllCasesSearchBar.tsx | 8 ++++- .../webapp/src/pages/AllCasesPage.tsx | 5 ++++ 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesFilters.tsx b/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesFilters.tsx index 1b51e92614..536178d64c 100644 --- a/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesFilters.tsx +++ b/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesFilters.tsx @@ -27,6 +27,7 @@ import type { SelectChangeEvent } from "@wso2/oxygen-ui"; import type { CaseMetadataResponse, AllCasesFilterValues, + ProjectDeploymentItem, } from "@models/responses"; import { ALL_CASES_FILTER_DEFINITIONS } from "@constants/supportConstants"; import { deriveFilterLabels, mapSeverityToDisplay } from "@utils/support"; @@ -34,6 +35,7 @@ import { deriveFilterLabels, mapSeverityToDisplay } from "@utils/support"; export interface AllCasesFiltersProps { filters: AllCasesFilterValues; filterMetadata: CaseMetadataResponse | undefined; + deployments?: ProjectDeploymentItem[]; onFilterChange: (field: string, value: string) => void; } @@ -46,6 +48,7 @@ export interface AllCasesFiltersProps { export default function AllCasesFilters({ filters, filterMetadata, + deployments, onFilterChange, }: AllCasesFiltersProps): JSX.Element { const handleSelectChange = @@ -58,16 +61,25 @@ export default function AllCasesFilters({ {ALL_CASES_FILTER_DEFINITIONS.map((def) => { const { label, allLabel } = deriveFilterLabels(def.id); - const metadataOptions = filterMetadata?.[def.metadataKey]; - const options = Array.isArray(metadataOptions) - ? metadataOptions.map((item: { label: string; id: string }) => ({ - label: - def.metadataKey === "severities" - ? mapSeverityToDisplay(item.label) - : item.label, - value: def.useLabelAsValue ? item.label : item.id, + + const isDeploymentFilter = def.id === "deployment"; + const options = isDeploymentFilter && deployments + ? deployments.map((deployment) => ({ + label: deployment.type?.label || deployment.name, + value: deployment.id, })) - : []; + : (() => { + const metadataOptions = filterMetadata?.[def.metadataKey]; + return Array.isArray(metadataOptions) + ? metadataOptions.map((item: { label: string; id: string }) => ({ + label: + def.metadataKey === "severities" + ? mapSeverityToDisplay(item.label) + : item.label, + value: def.useLabelAsValue ? item.label : item.id, + })) + : []; + })(); return ( diff --git a/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesSearchBar.tsx b/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesSearchBar.tsx index 557fc8c6d2..718ec52be8 100644 --- a/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesSearchBar.tsx +++ b/apps/customer-portal/webapp/src/components/support/all-cases/AllCasesSearchBar.tsx @@ -30,7 +30,10 @@ import { X, } from "@wso2/oxygen-ui-icons-react"; import type { JSX, ChangeEvent } from "react"; -import type { CaseMetadataResponse } from "@models/responses"; +import type { + CaseMetadataResponse, + ProjectDeploymentItem, +} from "@models/responses"; import AllCasesFilters from "@components/support/all-cases/AllCasesFilters"; export interface AllCasesSearchBarProps { @@ -45,6 +48,7 @@ export interface AllCasesSearchBarProps { deploymentId?: string; }; filterMetadata: CaseMetadataResponse | undefined; + deployments?: ProjectDeploymentItem[]; onFilterChange: (field: string, value: string) => void; onClearFilters: () => void; } @@ -62,6 +66,7 @@ export default function AllCasesSearchBar({ onFiltersToggle, filters, filterMetadata, + deployments, onFilterChange, onClearFilters, }: AllCasesSearchBarProps): JSX.Element { @@ -118,6 +123,7 @@ export default function AllCasesSearchBar({ diff --git a/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx b/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx index a897ad1b3c..a160b568b5 100644 --- a/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx +++ b/apps/customer-portal/webapp/src/pages/AllCasesPage.tsx @@ -38,6 +38,7 @@ import { ArrowLeft } from "@wso2/oxygen-ui-icons-react"; import { useGetProjectCasesStats } from "@api/useGetProjectCasesStats"; import useGetProjectFilters from "@api/useGetProjectFilters"; import useGetProjectCases from "@api/useGetProjectCases"; +import { useGetDeployments } from "@api/useGetDeployments"; import { getIncidentAndQueryIds } from "@utils/support"; import { CaseType } from "@constants/supportConstants"; import type { AllCasesFilterValues } from "@models/responses"; @@ -66,6 +67,9 @@ export default function AllCasesPage(): JSX.Element { // Fetch filter metadata first to get Incident and Query IDs for stats API const { data: filterMetadata } = useGetProjectFilters(projectId || ""); + // Fetch deployments for the deployment filter + const { data: deploymentsData } = useGetDeployments(projectId || ""); + const { incidentId, queryId } = useMemo( () => getIncidentAndQueryIds(filterMetadata?.caseTypes), [filterMetadata?.caseTypes], @@ -228,6 +232,7 @@ export default function AllCasesPage(): JSX.Element { onFiltersToggle={() => setIsFiltersOpen(!isFiltersOpen)} filters={filters} filterMetadata={filterMetadata} + deployments={deploymentsData?.deployments} onFilterChange={handleFilterChange} onClearFilters={handleClearFilters} /> From 73ec4700a10f14d2ecfef62163e757460a485b45 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 03:36:48 +0530 Subject: [PATCH 04/10] Pass conversationId when creating cases Add an optional conversationId to the CreateCaseRequest model and thread it through the UI flow so chat conversations can be linked to newly created cases. CreateCasePage: accept conversationId from location state and include it in the case payload when present. NoveraChatPage: include conversationId in navigation state when routing to the create-case page and add it to the useCallback dependency array. These changes ensure the chat conversation ID is preserved and sent with case creation requests. --- .../customer-portal/webapp/src/models/requests.ts | 2 +- .../webapp/src/pages/CreateCasePage.tsx | 4 ++++ .../webapp/src/pages/NoveraChatPage.tsx | 15 +++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/customer-portal/webapp/src/models/requests.ts b/apps/customer-portal/webapp/src/models/requests.ts index 47995c5bf4..75ff0c8ed1 100644 --- a/apps/customer-portal/webapp/src/models/requests.ts +++ b/apps/customer-portal/webapp/src/models/requests.ts @@ -104,8 +104,8 @@ export interface CreateCaseRequest { projectId: string; severityKey?: number; title: string; - /** Parent case ID when creating a related case from closed state. */ parentCaseId?: string; + conversationId?: string; } // Request body for product vulnerabilities search. diff --git a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx index 3ec8956235..03f5ca082f 100644 --- a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx +++ b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx @@ -206,6 +206,7 @@ export default function CreateCasePage(): JSX.Element { environment?: string; }; }; + conversationId?: string; } | null; const STORAGE_KEY = `case_classification_data_${projectId}`; @@ -653,6 +654,9 @@ export default function CreateCasePage(): JSX.Element { ...(relatedCase?.parentCaseId && { parentCaseId: relatedCase.parentCaseId, }), + ...(locationState?.conversationId && { + conversationId: locationState.conversationId, + }), }; postCase(payload, { diff --git a/apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx b/apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx index 4486c6ad08..2a866de301 100644 --- a/apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx +++ b/apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx @@ -160,23 +160,30 @@ export default function NoveraChatPage(): JSX.Element { tier: DEFAULT_CONVERSATION_TIER, }); navigate(`/${projectId}/support/chat/create-case`, { - state: { messages, classificationResponse }, + state: { messages, classificationResponse, conversationId }, }); } catch { navigate(`/${projectId}/support/chat/create-case`, { - state: { messages }, + state: { messages, conversationId }, }); } } else { navigate(`/${projectId}/support/chat/create-case`, { - state: { messages }, + state: { messages, conversationId }, }); } } finally { setIsCreateCaseLoading(false); setIsWaitingForClassification(false); } - }, [projectId, navigate, messages, envProducts, classifyCase]); + }, [ + projectId, + navigate, + messages, + envProducts, + classifyCase, + conversationId, + ]); const handleCreateCase = useCallback(() => { setIsCreateCaseLoading(true); From e15db187e8e18a4e76dc0913ee39c2e6daba7bb7 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 06:03:31 +0530 Subject: [PATCH 05/10] Populate Deployment filter from deployments API Fetch deployments in CasesTable and use them to populate the Deployment filter options. Adds useGetDeployments import and hook call (useGetDeployments(projectId)), replaces previous filtersMetadata.deploymentTypes usage with deploymentsData.deployments, and falls back to d.type?.label || d.name for the option label. This ensures the filter reflects actual deployments for the project. --- .../src/components/dashboard/cases-table/CasesTable.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx index 13f666862b..cf0c96c023 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx @@ -21,6 +21,7 @@ import { useAsgardeo } from "@asgardeo/react"; import { getNoveraChatEnabled } from "@utils/settingsStorage"; import { useGetProjectCasesPage } from "@api/useGetProjectCasesPage"; import useGetProjectFilters from "@api/useGetProjectFilters"; +import { useGetDeployments } from "@api/useGetDeployments"; import FilterPopover, { type FilterField, } from "@components/common/filter-panel/FilterPopover"; @@ -48,6 +49,9 @@ const CasesTable = ({ projectId }: CasesTableProps): JSX.Element => { isError: isErrorFilters, } = useGetProjectFilters(projectId); + // Fetch deployments for the deployment filter + const { data: deploymentsData } = useGetDeployments(projectId); + const dynamicFilterFields: FilterField[] = [ { id: "statusId", @@ -84,8 +88,8 @@ const CasesTable = ({ projectId }: CasesTableProps): JSX.Element => { label: "Deployment", type: "select", options: - filtersMetadata?.deploymentTypes?.map((d) => ({ - label: d.label, + deploymentsData?.deployments?.map((d) => ({ + label: d.type?.label || d.name, value: d.id, })) || [], }, From 985f4fe4fadacf42386191c1d17dcf0287795cba Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 06:09:57 +0530 Subject: [PATCH 06/10] Map severity labels for display Use mapSeverityToDisplay when building severity filter options in CasesTable so severity values are rendered with display-friendly labels. Added import from @utils/support and applied the mapper to the severity metadata mapping. --- .../src/components/dashboard/cases-table/CasesTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx index cf0c96c023..696223b85d 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesTable.tsx @@ -27,7 +27,7 @@ import FilterPopover, { } from "@components/common/filter-panel/FilterPopover"; import CasesTableHeader from "@components/dashboard/cases-table/CasesTableHeader"; import CasesList from "@components/dashboard/cases-table/CasesList"; -import { normalizeCaseTypeOptions } from "@utils/support"; +import { normalizeCaseTypeOptions, mapSeverityToDisplay } from "@utils/support"; const OUTSTANDING_STATUS_IDS = [1, 10, 18, 1003, 1006] as const; @@ -69,7 +69,7 @@ const CasesTable = ({ projectId }: CasesTableProps): JSX.Element => { type: "select", options: filtersMetadata?.severities?.map((s) => ({ - label: s.label, + label: mapSeverityToDisplay(s.label), value: s.id, })) || [], }, From 4f367181afa68f1f8243b77add1ac47d1a116c3f Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 06:11:50 +0530 Subject: [PATCH 07/10] Remove ExternalLink icon from CasesList Remove the ExternalLink import and its rendered icon from apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx. Cleans up an unused import and removes the external link icon displayed next to case titles. --- .../webapp/src/components/dashboard/cases-table/CasesList.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx index 5a6afcc3cd..f310067d89 100644 --- a/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx +++ b/apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx @@ -29,7 +29,6 @@ import { TablePagination, alpha, } from "@wso2/oxygen-ui"; -import { ExternalLink } from "@wso2/oxygen-ui-icons-react"; import { type JSX, type ChangeEvent } from "react"; import type { CaseSearchResponse, CaseListItem } from "@models/responses"; import { @@ -160,7 +159,6 @@ const CasesList = ({ }} > {row.title || "--"} - ID: {row.number} From 35508ddf87e7cdbe12b5d7ff80a14b50839099a2 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 06:14:28 +0530 Subject: [PATCH 08/10] Persist conversationId in sessionStorage Add CONVERSATION_ID_STORAGE_KEY and manage conversationId state so it survives page refreshes. Initialize conversationId from locationState or sessionStorage, sync updates to sessionStorage via useEffect, and prefer the stored conversationId when building the case payload. Also remove the stored conversationId on successful case creation and add console.error logging for sessionStorage failures. --- .../webapp/src/pages/CreateCasePage.tsx | 54 ++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx index 03f5ca082f..4a345b92c6 100644 --- a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx +++ b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx @@ -210,6 +210,7 @@ export default function CreateCasePage(): JSX.Element { } | null; const STORAGE_KEY = `case_classification_data_${projectId}`; + const CONVERSATION_ID_STORAGE_KEY = `case_conversation_id_${projectId}`; const [classificationResponse, setClassificationResponse] = useState< | { @@ -254,6 +255,54 @@ export default function CreateCasePage(): JSX.Element { setClassificationResponse(locationState.classificationResponse); } }, [locationState?.classificationResponse, STORAGE_KEY]); + + // Persist conversationId to survive page refresh + const [conversationId, setConversationId] = useState( + () => { + if (locationState?.conversationId) { + return locationState.conversationId; + } + try { + const stored = sessionStorage.getItem(CONVERSATION_ID_STORAGE_KEY); + return stored || undefined; + } catch (e) { + console.error("Failed to retrieve conversationId from sessionStorage", e); + return undefined; + } + }, + ); + + useEffect(() => { + if (locationState?.conversationId) { + try { + sessionStorage.setItem( + CONVERSATION_ID_STORAGE_KEY, + locationState.conversationId, + ); + } catch (e) { + console.error( + "Failed to store conversationId in sessionStorage", + e, + ); + } + setConversationId(locationState.conversationId); + } + }, [locationState?.conversationId, CONVERSATION_ID_STORAGE_KEY]); + + // Persist conversationId whenever it changes + useEffect(() => { + if (conversationId) { + try { + sessionStorage.setItem(CONVERSATION_ID_STORAGE_KEY, conversationId); + } catch (e) { + console.error( + "Failed to persist conversationId to sessionStorage", + e, + ); + } + } + }, [conversationId, CONVERSATION_ID_STORAGE_KEY]); + const projectDisplay = projectDetails?.name ?? ""; const issueTypesList = (filters?.issueTypes || []) as { @@ -654,8 +703,8 @@ export default function CreateCasePage(): JSX.Element { ...(relatedCase?.parentCaseId && { parentCaseId: relatedCase.parentCaseId, }), - ...(locationState?.conversationId && { - conversationId: locationState.conversationId, + ...(conversationId && { + conversationId, }), }; @@ -674,6 +723,7 @@ export default function CreateCasePage(): JSX.Element { showSuccess("Case created successfully"); sessionStorage.removeItem(STORAGE_KEY); + sessionStorage.removeItem(CONVERSATION_ID_STORAGE_KEY); // Refetch security vulnerabilities if this was a security report if (isCreatedSecurityReport) { From 238a3c8d052de85d58bce6737dbf620ddcdb0604 Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 06:16:40 +0530 Subject: [PATCH 09/10] Use logger instead of console in CreateCasePage Import and use the useLogger hook in CreateCasePage.tsx, replacing direct console.error calls with logger.error for centralized logging and better observability. Also add logger to relevant useEffect dependency arrays to satisfy hook dependency rules and avoid stale closures. --- .../webapp/src/pages/CreateCasePage.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx index 4a345b92c6..6b91945187 100644 --- a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx +++ b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx @@ -35,6 +35,7 @@ import { usePostCase } from "@api/usePostCase"; import { useLoader } from "@context/linear-loader/LoaderContext"; import { useErrorBanner } from "@context/error-banner/ErrorBannerContext"; import { useSuccessBanner } from "@context/success-banner/SuccessBannerContext"; +import { useLogger } from "@hooks/useLogger"; import type { CreateCaseRequest } from "@models/requests"; import { BasicInformationSection } from "@components/support/case-creation-layout/form-sections/basic-information-section/BasicInformationSection"; import { CaseCreationHeader } from "@components/support/case-creation-layout/header/CaseCreationHeader"; @@ -177,6 +178,7 @@ export default function CreateCasePage(): JSX.Element { const { showError } = useErrorBanner(); const { showSuccess } = useSuccessBanner(); const { mutate: postCase, isPending: isCreatePending } = usePostCase(); + const logger = useLogger(); useEffect(() => { if (deploymentProductsError) { @@ -266,7 +268,7 @@ export default function CreateCasePage(): JSX.Element { const stored = sessionStorage.getItem(CONVERSATION_ID_STORAGE_KEY); return stored || undefined; } catch (e) { - console.error("Failed to retrieve conversationId from sessionStorage", e); + logger.error("Failed to retrieve conversationId from sessionStorage", e); return undefined; } }, @@ -280,14 +282,14 @@ export default function CreateCasePage(): JSX.Element { locationState.conversationId, ); } catch (e) { - console.error( + logger.error( "Failed to store conversationId in sessionStorage", e, ); } setConversationId(locationState.conversationId); } - }, [locationState?.conversationId, CONVERSATION_ID_STORAGE_KEY]); + }, [locationState?.conversationId, CONVERSATION_ID_STORAGE_KEY, logger]); // Persist conversationId whenever it changes useEffect(() => { @@ -295,13 +297,13 @@ export default function CreateCasePage(): JSX.Element { try { sessionStorage.setItem(CONVERSATION_ID_STORAGE_KEY, conversationId); } catch (e) { - console.error( + logger.error( "Failed to persist conversationId to sessionStorage", e, ); } } - }, [conversationId, CONVERSATION_ID_STORAGE_KEY]); + }, [conversationId, CONVERSATION_ID_STORAGE_KEY, logger]); const projectDisplay = projectDetails?.name ?? ""; From d347bd0ab726675dbd5c3e60ce3e3a98bde7b96f Mon Sep 17 00:00:00 2001 From: "K.Dileepa Thushan Peiris" Date: Tue, 3 Mar 2026 06:31:15 +0530 Subject: [PATCH 10/10] Use logger and add storage/export error handling Replace console.error with logger.error in CreateCasePage and add logger to effect deps. Make sessionStorage interactions more robust by wrapping reads/writes/removals in try/catch, removing the conversationId key when undefined, and logging failures during cleanup after case creation. In ChangeRequestsPage, include stats loading/error flags in the export completion effect and add them to the effect dependency list so exports wait for stats and properly handle stats errors. These changes improve error reporting and resilience for sessionStorage and export flows. --- .../webapp/src/pages/ChangeRequestsPage.tsx | 7 ++-- .../webapp/src/pages/CreateCasePage.tsx | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx b/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx index e4e712f6a8..9e9089b1fc 100644 --- a/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx +++ b/apps/customer-portal/webapp/src/pages/ChangeRequestsPage.tsx @@ -168,12 +168,13 @@ export default function ChangeRequestsPage(): JSX.Element { // Handle export completion when all data is fetched useEffect(() => { if (isExporting) { - if (isInfiniteError) { + if (isInfiniteError || isStatsError) { // Handle error case - console.error("Failed to fetch change requests for export"); + console.error("Failed to fetch change requests or stats for export"); setTimeout(() => setIsExporting(false), 0); } else if ( !isInfiniteLoading && + !isStatsLoading && !hasNextPage && !isFetchingNextPage && infiniteData && @@ -193,6 +194,8 @@ export default function ChangeRequestsPage(): JSX.Element { isExporting, isInfiniteLoading, isInfiniteError, + isStatsLoading, + isStatsError, hasNextPage, isFetchingNextPage, infiniteData, diff --git a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx index 6b91945187..dfa210855e 100644 --- a/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx +++ b/apps/customer-portal/webapp/src/pages/CreateCasePage.tsx @@ -236,7 +236,7 @@ export default function CreateCasePage(): JSX.Element { const stored = sessionStorage.getItem(STORAGE_KEY); return stored ? JSON.parse(stored) : undefined; } catch (e) { - console.error("Failed to parse stored classification data", e); + logger.error("Failed to parse stored classification data", e); return undefined; } }); @@ -249,14 +249,14 @@ export default function CreateCasePage(): JSX.Element { JSON.stringify(locationState.classificationResponse), ); } catch (e) { - console.error( + logger.error( "Failed to store classification data in sessionStorage", e, ); } setClassificationResponse(locationState.classificationResponse); } - }, [locationState?.classificationResponse, STORAGE_KEY]); + }, [locationState?.classificationResponse, STORAGE_KEY, logger]); // Persist conversationId to survive page refresh const [conversationId, setConversationId] = useState( @@ -293,15 +293,17 @@ export default function CreateCasePage(): JSX.Element { // Persist conversationId whenever it changes useEffect(() => { - if (conversationId) { - try { + try { + if (conversationId) { sessionStorage.setItem(CONVERSATION_ID_STORAGE_KEY, conversationId); - } catch (e) { - logger.error( - "Failed to persist conversationId to sessionStorage", - e, - ); + } else { + sessionStorage.removeItem(CONVERSATION_ID_STORAGE_KEY); } + } catch (e) { + logger.error( + "Failed to persist conversationId to sessionStorage", + e, + ); } }, [conversationId, CONVERSATION_ID_STORAGE_KEY, logger]); @@ -724,8 +726,14 @@ export default function CreateCasePage(): JSX.Element { ); showSuccess("Case created successfully"); - sessionStorage.removeItem(STORAGE_KEY); - sessionStorage.removeItem(CONVERSATION_ID_STORAGE_KEY); + + // Clean up sessionStorage safely + try { + sessionStorage.removeItem(STORAGE_KEY); + sessionStorage.removeItem(CONVERSATION_ID_STORAGE_KEY); + } catch (e) { + logger.error("Failed to cleanup sessionStorage after case creation", e); + } // Refetch security vulnerabilities if this was a security report if (isCreatedSecurityReport) {