Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
37f23dd
Add Service Requests list components
Suwagath Feb 23, 2026
eca2365
Add ServiceRequestsPage for listing all service requests
Suwagath Feb 23, 2026
4b49adc
Wire up Service Requests page route and navigation
Suwagath Feb 23, 2026
b7c4973
Refine service requests list UI
Suwagath Feb 26, 2026
f642628
Integrate service requests page with case search API
Suwagath Feb 26, 2026
e1deb51
Show environment, product, and team on service request cards
Suwagath Feb 26, 2026
818b260
Add dedicated service request details route and page
Suwagath Feb 26, 2026
86a9136
Implement service request detail view UI
Suwagath Feb 27, 2026
a4ad9a4
feat(customer-portal): add Create Service Request page with catalog flow
Suwagath Mar 2, 2026
d397a1f
feat(service-requests): RTE for description, attachments, required as…
Suwagath Mar 2, 2026
3a339df
fix: update useGetCaseAttachments test mock for CaseAttachment type
Suwagath Mar 2, 2026
3226efe
fix: service request creation - attachment fields, validation, payload
Suwagath Mar 2, 2026
ea686ef
refactor: enhance service request timeline with actor details and imp…
Suwagath Mar 2, 2026
4912a7b
feat: add ServiceRequestsStatCards component for service request stat…
Suwagath Mar 2, 2026
6a9eae4
feat: enhance RequestCard and ServiceRequestsPage with footer buttons…
Suwagath Mar 2, 2026
32ccf7a
feat: enhance ServiceRequestsSearchBar with dark mode support and sta…
Suwagath Mar 2, 2026
0b58e6f
refactor: simplify styling in RequestCard and ServiceRequestDetailCon…
Suwagath Mar 2, 2026
0a7f3c0
feat: enhance ServiceRequestDetailContent and VariableFormFields with…
Suwagath Mar 2, 2026
d2154de
feat: filter out specific context fields in ServiceRequestDetailConte…
Suwagath Mar 2, 2026
3bd3bd7
feat: add Cancel button to CreateServiceRequestPage for improved navi…
Suwagath Mar 2, 2026
947ed66
refactor: simplify styling in CatalogSelector component
Suwagath Mar 2, 2026
08a55e6
refactor: clean up ServiceRequestDetailContent and CreateServiceReque…
Suwagath Mar 2, 2026
a8f1daa
refactor: update ServiceRequestDetailContent to use project filters
Suwagath Mar 2, 2026
ad32f29
refactor: remove CORS
Suwagath Mar 2, 2026
db01659
fix: sanitize usePostCase payload logging to avoid leaking sensitive …
Suwagath Mar 2, 2026
6d216b5
fix: add onError for comment mutation and fallback when Request Detai…
Suwagath Mar 2, 2026
a9b6fbb
fix: map tab value in_progress to stats.inProgress for correct count
Suwagath Mar 2, 2026
72a53ed
fix: remove duplicate ChangeRequestFilterValues interface (use from @…
Suwagath Mar 2, 2026
efa97d5
fix: clear attachments on deployment/product/catalog change; allow su…
Suwagath Mar 2, 2026
3c2805f
fix: add error handling for cases fetch; unify status bucketing via c…
Suwagath Mar 2, 2026
a58dc1f
fix: add comments fetch error/loading UI; filter WSO2 Product from Re…
Suwagath Mar 2, 2026
5c96929
fix: remove redundant type-guards for deploymentId/deployedProductId …
Suwagath Mar 2, 2026
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
14 changes: 14 additions & 0 deletions apps/customer-portal/webapp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import AnnouncementDetailsPage from "@pages/AnnouncementDetailsPage";
import AllConversationsPage from "@pages/AllConversationsPage";
import ConversationDetailsPage from "@pages/ConversationDetailsPage";
import CaseDetailsPage from "@pages/CaseDetailsPage";
import ServiceRequestsPage from "@pages/ServiceRequestsPage";
import ServiceRequestDetailsPage from "@pages/ServiceRequestDetailsPage";
import CreateServiceRequestPage from "@pages/CreateServiceRequestPage";
import NoveraChatPage from "@pages/NoveraChatPage";
import DescribeIssuePage from "@pages/DescribeIssuePage";
import CreateCasePage from "@pages/CreateCasePage";
Expand Down Expand Up @@ -98,6 +101,17 @@ export default function App(): JSX.Element {
element={<ConversationDetailsPage />}
/>
</Route>
<Route path="service-requests">
<Route index element={<ServiceRequestsPage />} />
<Route
path="create"
element={<CreateServiceRequestPage />}
/>
<Route
path=":serviceRequestId"
element={<ServiceRequestDetailsPage />}
/>
</Route>
<Route path="chat">
<Route index element={<NoveraChatPage />} />
<Route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,24 @@ import {

const mockAttachmentsPage1 = {
attachments: [
{ id: "a1", name: "file1.txt", type: "text/plain", size: 100, downloadUrl: "/a1", createdOn: "", createdBy: "" },
{ id: "a2", name: "file2.txt", type: "text/plain", size: 200, downloadUrl: "/a2", createdOn: "", createdBy: "" },
{
id: "a1",
name: "file1.txt",
type: "text/plain",
size: 100,
downloadUrl: "/a1",
createdOn: "",
createdBy: "",
},
{
id: "a2",
name: "file2.txt",
type: "text/plain",
size: 200,
downloadUrl: "/a2",
createdOn: "",
createdBy: "",
},
],
totalRecords: 12,
offset: 0,
Expand All @@ -34,7 +50,15 @@ const mockAttachmentsPage1 = {

const mockAttachmentsPage2 = {
attachments: [
{ id: "a3", name: "file3.txt", type: "text/plain", size: 300, downloadUrl: "/a3", createdOn: "", createdBy: "" },
{
id: "a3",
name: "file3.txt",
type: "text/plain",
size: 300,
downloadUrl: "/a3",
createdOn: "",
createdBy: "",
},
],
totalRecords: 12,
offset: 10,
Expand Down
72 changes: 72 additions & 0 deletions apps/customer-portal/webapp/src/api/useGetCatalogItemVariables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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 { useAuthApiClient } from "@context/AuthApiContext";
import { useLogger } from "@hooks/useLogger";
import { ApiQueryKeys } from "@constants/apiConstants";
import type { CatalogItemVariablesResponse } from "@models/responses";

/**
* Fetches catalog item variables (GET /catalogs/:catalogId/items/:itemId).
*
* @param {string} catalogId - The catalog ID.
* @param {string} itemId - The catalog item ID.
* @returns {UseQueryResult<CatalogItemVariablesResponse, Error>} The query result.
*/
export function useGetCatalogItemVariables(
catalogId: string,
itemId: string,
): UseQueryResult<CatalogItemVariablesResponse, Error> {
const logger = useLogger();
const { isSignedIn, isLoading: isAuthLoading } = useAsgardeo();
const fetchFn = useAuthApiClient();

return useQuery<CatalogItemVariablesResponse, Error>({
queryKey: [ApiQueryKeys.CATALOG_ITEM_VARIABLES, catalogId, itemId],
queryFn: async (): Promise<CatalogItemVariablesResponse> => {
logger.debug(
`Fetching catalog item variables for catalog ${catalogId}, item ${itemId}`,
);

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}/catalogs/${catalogId}/items/${itemId}`;

const response = await fetchFn(requestUrl, { method: "GET" });

if (!response.ok) {
const text = await response.text();
throw new Error(
`Error fetching catalog item variables: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}

const data: CatalogItemVariablesResponse = await response.json();
logger.debug(
`Catalog item variables fetched for catalog ${catalogId}, item ${itemId}`,
data,
);
return data;
},
enabled:
!!catalogId && !!itemId && isSignedIn && !isAuthLoading,
staleTime: 5 * 60 * 1000,
});
}
35 changes: 24 additions & 11 deletions apps/customer-portal/webapp/src/api/usePostCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,45 @@ import { useMutation, type UseMutationResult } from "@tanstack/react-query";
import { useAsgardeo } from "@asgardeo/react";
import { useLogger } from "@hooks/useLogger";
import { useAuthApiClient } from "@context/AuthApiContext";
import type { CreateCaseRequest } from "@models/requests";
import type {
CreateCaseRequest,
CreateServiceRequestPayload,
} from "@models/requests";
import type { CreateCaseResponse } from "@models/responses";

/**
* Posts a new support case to the backend.
* Posts a new support case or service request to the backend.
*
* @returns {UseMutationResult<CreateCaseResponse, Error, CreateCaseRequest>} Mutation result.
* @returns {UseMutationResult<CreateCaseResponse, Error, CreateCaseRequest | CreateServiceRequestPayload>} Mutation result.
*/
export function usePostCase(): UseMutationResult<
CreateCaseResponse,
Error,
CreateCaseRequest
CreateCaseRequest | CreateServiceRequestPayload
> {
const logger = useLogger();
const { isSignedIn, isLoading: isAuthLoading } = useAsgardeo();
const fetchFn = useAuthApiClient();

return useMutation<CreateCaseResponse, Error, CreateCaseRequest>({
return useMutation<
CreateCaseResponse,
Error,
CreateCaseRequest | CreateServiceRequestPayload
>({
mutationFn: async (
body: CreateCaseRequest,
body: CreateCaseRequest | CreateServiceRequestPayload,
): Promise<CreateCaseResponse> => {
logger.debug("[usePostCase] Request payload:", {
...body,
description: body.description
? `${body.description.slice(0, 80)}...`
: "",
logger.debug("[usePostCase] Request payload summary:", {
requestType: "type" in body ? body.type : "case",
projectId: body.projectId,
deploymentId: body.deploymentId,
deployedProductId: body.deployedProductId,
descriptionPreview:
"description" in body && body.description
? `${body.description.slice(0, 80)}...`
: undefined,
variableCount: "variables" in body ? body.variables.length : undefined,
attachmentCount: body.attachments?.length ?? 0,
});

try {
Expand Down
73 changes: 73 additions & 0 deletions apps/customer-portal/webapp/src/api/useSearchCatalogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 { useAuthApiClient } from "@context/AuthApiContext";
import { useLogger } from "@hooks/useLogger";
import { ApiQueryKeys } from "@constants/apiConstants";
import type { CatalogSearchResponse } from "@models/responses";

/**
* Searches catalogs for a deployed product (POST /deployments/products/:deployedProductId/catalogs/search).
*
* @param {string} deployedProductId - The deployed product ID.
* @returns {UseQueryResult<CatalogSearchResponse, Error>} The query result.
*/
export function useSearchCatalogs(
deployedProductId: string,
): UseQueryResult<CatalogSearchResponse, Error> {
const logger = useLogger();
const { isSignedIn, isLoading: isAuthLoading } = useAsgardeo();
const fetchFn = useAuthApiClient();

return useQuery<CatalogSearchResponse, Error>({
queryKey: [ApiQueryKeys.CATALOGS_SEARCH, deployedProductId],
queryFn: async (): Promise<CatalogSearchResponse> => {
logger.debug(
`Searching catalogs for deployed product ID: ${deployedProductId}`,
);

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}/deployments/products/${deployedProductId}/catalogs/search`;

const response = await fetchFn(requestUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});

if (!response.ok) {
const text = await response.text();
throw new Error(
`Error searching catalogs: ${response.status} ${response.statusText}${text ? ` - ${text}` : ""}`,
);
}

const data: CatalogSearchResponse = await response.json();
logger.debug(
`Catalogs fetched for deployed product ID: ${deployedProductId}`,
data,
);
return data;
},
enabled: !!deployedProductId && isSignedIn && !isAuthLoading,
staleTime: 5 * 60 * 1000,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ export default function GetHelpDropdown(): JSX.Element {
const handleServiceRequest = () => {
handleClose();
if (projectId) {
navigate(`/${projectId}/support/chat/create-case?type=service_request`, {
state: { skipChat: true },
});
navigate(`/${projectId}/support/service-requests/create`);
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Paper,
Typography,
alpha,
colors,
useTheme,
} from "@wso2/oxygen-ui";
import { ArrowRight } from "@wso2/oxygen-ui-icons-react";
Expand All @@ -37,8 +38,12 @@ export interface RequestCardProps {
infoBoxTitle: string;
infoBoxDescription: string;
bulletItems: readonly string[];
secondaryButtonLabel: string;
onSecondaryClick: () => void;
secondaryButtonLabel?: string;
onSecondaryClick?: () => void;
footerButtons?: Array<{
label: string;
onClick?: () => void;
}>;
primaryButton?: {
label: string;
onClick: () => void;
Expand All @@ -64,6 +69,7 @@ export default function RequestCard({
bulletItems,
secondaryButtonLabel,
onSecondaryClick,
footerButtons,
primaryButton,
}: RequestCardProps): JSX.Element {
const theme = useTheme();
Expand Down Expand Up @@ -179,20 +185,46 @@ export default function RequestCard({
{primaryButton.label}
</Button>
)}
<Button
fullWidth
variant="outlined"
color="inherit"
endIcon={<ArrowRight size={16} />}
onClick={onSecondaryClick}
sx={{
textTransform: "none",
borderColor: "divider",
color: "text.primary",
}}
>
{secondaryButtonLabel}
</Button>
{footerButtons && footerButtons.length > 0 ? (
<Box sx={{ display: "flex", gap: 1 }}>
{footerButtons.map((btn, index) => (
<Button
key={index}
fullWidth
variant="text"
color="info"
onClick={btn.onClick}
endIcon={<ArrowRight size={16} />}
sx={{
flex: 1,
justifyContent: "flex-start",
textTransform: "none",
fontWeight: 500,
color: colors.blue[600],
}}
>
{btn.label}
</Button>
))}
</Box>
) : (
secondaryButtonLabel &&
onSecondaryClick && (
<Button
fullWidth
variant="outlined"
color="inherit"
endIcon={<ArrowRight size={16} />}
onClick={onSecondaryClick}
sx={{
textTransform: "none",
color: "text.primary",
}}
>
{secondaryButtonLabel}
</Button>
)
)}
</Box>
</Paper>
);
Expand Down
Loading