From 7650f7598b2d4c055efc05961539c8bfc2758b18 Mon Sep 17 00:00:00 2001 From: Rashmika998 Date: Fri, 26 Jun 2026 20:45:01 +0530 Subject: [PATCH 1/2] [CSM Portal][FE] feat: implement engagements search and detail navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the coming-soon placeholder at /engagements with a fully working engagements list backed by POST /cases/search (type: engagement). - Add BeEngagementType union type and engagementTypes filter to BeCaseSearchFilters in api/backend/types.ts - Extend CasesFilters with engagementTypes; add engagement-type multi-select to CasesFilterBar behind showEngagementTypeFilter prop - Persist engagementTypes in URL via casesFiltersUrl read/write/count - Pass engagementTypes in useGetCsmCases query key and search payload - Add detailBasePath and showEngagementTypeFilter props to CsmIssuesView; thread detailBasePath down to CasesList row links - CsmCaseDetailPage: derive back label/path from location.pathname so /engagements/:id shows "Back to engagements" and /cases/:id is unchanged - New CsmEngagementsPage using CsmIssuesView with locked engagement type, engagement-type filter, and detailBasePath="/engagements" - Route /engagements → CsmEngagementsPage; /engagements/:caseId → CsmCaseDetailPage Co-Authored-By: Claude Sonnet 4.6 --- apps/csm-portal/webapp/src/App.tsx | 16 +++----- .../webapp/src/api/backend/types.ts | 9 +++++ .../features/csm-cases/api/useGetCsmCases.ts | 4 ++ .../csm-cases/components/CasesFilterBar.tsx | 38 ++++++++++++++++++- .../csm-cases/components/CasesList.tsx | 5 ++- .../csm-cases/components/CsmIssuesView.tsx | 10 ++++- .../csm-cases/pages/CsmCaseDetailPage.tsx | 15 +++++--- .../csm-cases/utils/casesFiltersUrl.ts | 13 ++++++- .../pages/CsmEngagementsPage.tsx | 36 ++++++++++++++++++ 9 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 apps/csm-portal/webapp/src/features/csm-engagements/pages/CsmEngagementsPage.tsx diff --git a/apps/csm-portal/webapp/src/App.tsx b/apps/csm-portal/webapp/src/App.tsx index 5611eae18f..a302e4239a 100644 --- a/apps/csm-portal/webapp/src/App.tsx +++ b/apps/csm-portal/webapp/src/App.tsx @@ -86,6 +86,9 @@ const CsmSecurityCenterPage = lazy( const CreateSecurityReportPage = lazy( () => import("@features/csm-security-center/pages/CreateSecurityReportPage"), ); +const CsmEngagementsPage = lazy( + () => import("@features/csm-engagements/pages/CsmEngagementsPage"), +); /** * Landing for `/`. Defers to AuthGuard's post-login deep-link restore when a @@ -233,17 +236,8 @@ export default function App(): JSX.Element { element={} /> - {/* WIP placeholders for top-level features awaiting BFF support */} - - } - /> + } /> + } /> } /> } /> 0 && { types: filters.caseTypes, }), + ...(filters.engagementTypes.length > 0 && { + engagementTypes: filters.engagementTypes, + }), // `filters.projects` holds project IDs (the filter is id-based). ...(filters.projects.length > 0 && { projectIds: filters.projects, diff --git a/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx b/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx index b114dbabfc..7ab9183cea 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx @@ -70,7 +70,7 @@ import { SUGGESTED_FILTER_VIEWS, useSavedFilterViews, } from "@features/csm-cases/utils/savedFilterViews"; -import type { BeCaseType } from "@api/backend/types"; +import type { BeCaseType, BeEngagementType } from "@api/backend/types"; import { ALL_CASE_TYPES, CASE_TYPE_LABEL, @@ -97,6 +97,8 @@ export interface CasesFilters { /** Engineer emails (+ the `@me` sentinel) to filter by assigned engineer. */ assignees: string[]; projects: string[]; + /** Engagement sub-type filter; only meaningful when `caseTypes` is locked to `engagement`. */ + engagementTypes: BeEngagementType[]; } /** @@ -122,8 +124,26 @@ interface CasesFilterBarProps { hideTypeFilter?: boolean; /** Hide the project control when the surrounding view is project-scoped. */ hideProjectFilter?: boolean; + /** Show the engagement-type multi-select (only relevant when type is locked to engagement). */ + showEngagementTypeFilter?: boolean; } +const ALL_ENGAGEMENT_TYPES: BeEngagementType[] = [ + "migration", + "consultancy", + "new_feature_improvement", + "follow_up", + "onboarding", +]; + +const ENGAGEMENT_TYPE_LABEL: Record = { + migration: "Migration", + consultancy: "Consultancy", + new_feature_improvement: "New feature / improvement", + follow_up: "Follow-up", + onboarding: "Onboarding", +}; + const ALL_SEVERITIES: Severity[] = ["S0", "S1", "S2", "S3", "S4"]; const PRIMARY_STATES: CaseState[] = [ "open", @@ -319,6 +339,7 @@ export default function CasesFilterBar({ availableProjects, hideTypeFilter = false, hideProjectFilter = false, + showEngagementTypeFilter = false, }: CasesFilterBarProps): JSX.Element { const activeCount = countActiveFilters(filters); const hasActive = activeCount > 0; @@ -368,6 +389,10 @@ export default function CasesFilterBar({ () => ALL_CASE_TYPES.map((t) => ({ value: t, label: CASE_TYPE_LABEL[t] })), [], ); + const engagementTypeOptions = useMemo( + () => ALL_ENGAGEMENT_TYPES.map((t) => ({ value: t, label: ENGAGEMENT_TYPE_LABEL[t] })), + [], + ); // Project filter loads the first page of projects on open and pages through // the rest on scroll (and narrows as you type) rather than loading the whole @@ -610,6 +635,17 @@ export default function CasesFilterBar({ onChange={(next) => onChange({ ...filters, states: next })} /> + {showEngagementTypeFilter && ( + + onChange({ ...filters, engagementTypes: next })} + /> + + )} {!hideTypeFilter && ( ( @@ -227,9 +234,10 @@ export default function CsmIssuesView({ availableProjects={availableProjects} hideTypeFilter={hideTypeFilter} hideProjectFilter={hideProjectFilter} + showEngagementTypeFilter={showEngagementTypeFilter} /> - + (); const navigate = useNavigate(); const location = useLocation(); + const isEngagementRoute = location.pathname.startsWith("/engagements/"); + const backPath = isEngagementRoute ? "/engagements" : "/cases"; + const backLabel = isEngagementRoute ? "Back to engagements" : "Back to cases"; const { data, isLoading, isError } = useGetCsmCaseDetail(caseId); const { data: comments, @@ -694,10 +697,10 @@ export default function CsmCaseDetailPage(): JSX.Element { variant="text" size="small" startIcon={} - onClick={() => navigate("/cases")} + onClick={() => navigate(backPath)} sx={{ alignSelf: "flex-start" }} > - Back to cases + {backLabel} Could not load case {caseId}. @@ -713,10 +716,10 @@ export default function CsmCaseDetailPage(): JSX.Element { variant="text" size="small" startIcon={} - onClick={() => navigate("/cases")} + onClick={() => navigate(backPath)} sx={{ alignSelf: "flex-start" }} > - Back to cases + {backLabel} Case not found @@ -771,10 +774,10 @@ export default function CsmCaseDetailPage(): JSX.Element { variant="text" size="small" startIcon={} - onClick={() => navigate("/cases")} + onClick={() => navigate(backPath)} sx={{ alignSelf: "flex-start" }} > - Back to cases + {backLabel} (raw: string | null, allowed: T[]): T[] { if (!raw) return []; @@ -73,6 +81,7 @@ export function readCasesFiltersFromUrl( caseTypes: parseCsv(params.get("types"), VALID_CASE_TYPES), assignees: parseFreeFormCsv(params.get("assignees")), projects: parseFreeFormCsv(params.get("projects")), + engagementTypes: parseCsv(params.get("engagementTypes"), VALID_ENGAGEMENT_TYPES), }; } @@ -88,6 +97,7 @@ export function writeCasesFiltersToUrl(f: CasesFilters): URLSearchParams { if (f.caseTypes.length) out.set("types", f.caseTypes.join(",")); if (f.assignees.length) out.set("assignees", f.assignees.join(",")); if (f.projects.length) out.set("projects", f.projects.join(",")); + if (f.engagementTypes.length) out.set("engagementTypes", f.engagementTypes.join(",")); return out; } @@ -105,6 +115,7 @@ export function countActiveFilters(f: CasesFilters): number { if (f.caseTypes.length) n += 1; if (f.assignees.length) n += 1; if (f.projects.length) n += 1; + if (f.engagementTypes.length) n += 1; return n; } diff --git a/apps/csm-portal/webapp/src/features/csm-engagements/pages/CsmEngagementsPage.tsx b/apps/csm-portal/webapp/src/features/csm-engagements/pages/CsmEngagementsPage.tsx new file mode 100644 index 0000000000..6a7d6efdca --- /dev/null +++ b/apps/csm-portal/webapp/src/features/csm-engagements/pages/CsmEngagementsPage.tsx @@ -0,0 +1,36 @@ +// 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 { type JSX } from "react"; +import CsmIssuesView from "@features/csm-cases/components/CsmIssuesView"; + +/** + * Cross-customer engagements list — filters the shared issues view to + * `type: engagement` and surfaces the engagement-type sub-filter so engineers + * can narrow by migration, consultancy, onboarding, etc. + */ +export default function CsmEngagementsPage(): JSX.Element { + return ( + + ); +} From d0f0e00a1edc8a25592e93fc76b4e4a14992deb1 Mon Sep 17 00:00:00 2001 From: Rashmika998 Date: Fri, 26 Jun 2026 20:57:39 +0530 Subject: [PATCH 2/2] Address CodeRabbit review: derive detailPath for recordView and copy-link Use the same isEngagementRoute flag to build a detailPath that is either /engagements/:id or /cases/:id, then reuse it for the recent-view href and the clipboard writeText so both stay on the correct route family. Co-Authored-By: Claude Sonnet 4.6 --- .../src/features/csm-cases/pages/CsmCaseDetailPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx index ad59823ad6..d3fd31098f 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx @@ -244,6 +244,7 @@ export default function CsmCaseDetailPage(): JSX.Element { const isEngagementRoute = location.pathname.startsWith("/engagements/"); const backPath = isEngagementRoute ? "/engagements" : "/cases"; const backLabel = isEngagementRoute ? "Back to engagements" : "Back to cases"; + const detailPath = isEngagementRoute ? `/engagements/${caseId}` : `/cases/${caseId}`; const { data, isLoading, isError } = useGetCsmCaseDetail(caseId); const { data: comments, @@ -369,7 +370,7 @@ export default function CsmCaseDetailPage(): JSX.Element { ? `${caseIdLabel(data)} · ${data.subject}` : data.subject, subtitle: `${data.customer} · ${data.projectName}`, - href: `/cases/${data.id}`, + href: detailPath, }); }, [data, recordView]); @@ -551,7 +552,7 @@ export default function CsmCaseDetailPage(): JSX.Element { if (action.secondary === "copy_link") { if (data && navigator.clipboard) { navigator.clipboard - .writeText(`${window.location.origin}/cases/${data.id}`) + .writeText(`${window.location.origin}${detailPath}`) .then(() => setFeedback({ message: SECONDARY_TOAST.copy_link,