Skip to content
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
45 changes: 45 additions & 0 deletions apps/csm-portal/webapp/src/components/WorkStateChip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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 { Chip } from "@wso2/oxygen-ui";
import type { JSX } from "react";
import type { CaseWorkState } from "@features/csm-dashboard/types/abtDashboard";
import { WORK_STATE_LABEL } from "@features/csm-cases/utils/caseWorkState";

interface WorkStateChipProps {
workState: CaseWorkState;
size?: "small" | "medium";
}

/**
* The single source of truth for the work sub-state badge (Ongoing/Paused)
* shown beside a `work_in_progress` case's state chip — same "single source
* of truth" role `SeverityChip`/`StateChip` play for their own badges.
*/
export default function WorkStateChip({
workState,
size = "small",
}: WorkStateChipProps): JSX.Element {
return (
<Chip
size={size}
variant="outlined"
color={workState === "paused" ? "warning" : "default"}
label={WORK_STATE_LABEL[workState]}
sx={{ fontWeight: 600 }}
/>
);
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ function detailFromBeCase(
const customer = account?.name ?? "—";
// createdBy.name can be empty for unhydrated users, so fall back to the email.
const reporter = c.createdBy?.name?.trim() || c.createdBy?.email;
const assignee = c.assignedEngineer?.name?.trim() || "Unassigned";
const assigneeName = c.assignedEngineer?.name?.trim() || undefined;
const assignee = assigneeName || "Unassigned";
// "Is me" by comparing the assignee's email (the only stable identity the FE
// shares with the JWT) to the signed-in user's, case-insensitively. Falls back
// to false when either is absent — e.g. the data source doesn't return the
Expand Down Expand Up @@ -71,6 +72,7 @@ function detailFromBeCase(
? { id: c.relatedCase.id, caseNumber: c.relatedCase.number }
: undefined,
assignee,
assigneeName,
assigneeIsMe,
slaClockType: "ack",
minutesToBreach: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
import { ApiQueryKeys } from "@constants/apiConstants";
import { useBackendApi } from "@api/backend/client";
import { severityFromPriority, uiStateFromBe } from "@api/backend/mappers";
import type {
BeCaseSearchPayload,
BeCaseSearchResponse,
BeCaseType,
} from "@api/backend/types";
import type {
CaseState,
CaseWorkState,
Severity,
} from "@features/csm-dashboard/types/abtDashboard";

/** Don't fire a search until the user has typed something searchable. */
export const QUICK_CASE_MIN_QUERY_LEN = 2;
Expand All @@ -29,15 +36,23 @@ export const QUICK_CASE_MIN_QUERY_LEN = 2;
const QUICK_CASE_LIMIT = 8;

/**
* One hit from the global-search case lookup. Carries only what the palette
* needs: the UUID `id` for the `/cases/:id` link and the human-readable
* identity/subject for display.
* One hit from the global-search case lookup. Carries the UUID `id` (for the
* `/cases/:id` link), the human-readable identity/subject, and enough of the
* case's severity/status/ownership for the palette to render a result card
* matching the case list's visual language.
*/
export interface QuickCaseHit {
id: string;
caseNumber?: string;
wso2CaseId?: string;
subject: string;
severity: Severity;
state: CaseState;
workState?: CaseWorkState | null;
caseType?: BeCaseType;
updatedOn?: string;
createdOn?: string;
assigneeName?: string;
}

/**
Expand Down Expand Up @@ -70,6 +85,13 @@ export function useQuickCaseSearch(
caseNumber: c.number,
wso2CaseId: c.internalId,
subject: c.subject ?? "(no subject)",
severity: severityFromPriority(c.severity),
state: uiStateFromBe(c.state),
workState: c.workState,
caseType: c.type,
updatedOn: c.updatedOn,
createdOn: c.createdOn,
assigneeName: c.assignedEngineer?.name,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}));
},
enabled: q.length >= QUICK_CASE_MIN_QUERY_LEN,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import {
Box,
Chip,
Skeleton,
TableSortLabel,
Typography,
Expand All @@ -28,7 +27,7 @@ import { preloadRoute } from "@utils/routePreloaders";
import RelativeTime from "@components/RelativeTime";
import SeverityChip from "@components/SeverityChip";
import StateChip from "@components/StateChip";
import { WORK_STATE_LABEL } from "@features/csm-cases/utils/caseWorkState";
import WorkStateChip from "@components/WorkStateChip";
import type { CsmCaseRow } from "@features/csm-cases/types/csmCases";
import type { CasesSortOrder } from "@features/csm-cases/utils/casesSort";

Expand Down Expand Up @@ -263,13 +262,7 @@ export default function CasesList({
>
<StateChip state={c.state} variant="outlined" clickable />
{c.state === "work_in_progress" && c.workState && (
<Chip
size="small"
variant="outlined"
color={c.workState === "paused" ? "warning" : "default"}
label={WORK_STATE_LABEL[c.workState]}
sx={{ fontWeight: 600 }}
/>
<WorkStateChip workState={c.workState} />
)}
</Box>
<Typography variant="caption" color="text.secondary" noWrap>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,20 @@ export default function CsmCaseDetailPage(): JSX.Element {
: data.subject,
subtitle: `${data.customer} · ${data.projectName}`,
href: detailPath,
// Snapshot of the fields QuickNavCaseCard needs, so Pinned/Recent
// entries for this case render the same rich card a live search hit
// does, without the palette re-fetching it.
caseHit: {
caseNumber: data.caseNumber,
wso2CaseId: data.wso2CaseId,
subject: data.subject,
severity: data.severity,
state: data.state,
workState: data.workState,
caseType: data.caseType,
updatedOn: data.updatedAt,
assigneeName: data.assigneeName,
},
});
}, [data, recordView]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ export interface CsmCaseDetail extends CsmCaseRow {
createdBy?: string;
/** Email of the creator — used to tell a WSO2 engineer from a customer. */
createdByEmail?: string;
/**
* Raw assigned-engineer name, with no "Unassigned" display fallback baked
* in (unlike `assignee`) — for callers that need to tell "actually
* unassigned" apart from a display label, without string-comparing
* against that fallback.
*/
assigneeName?: string;
customerContext: CaseCustomerContext;
productContext: CaseProductContext;
watchers: CaseWatcher[];
Expand Down
Loading