From 3af21b533ea9e2f0dffa36aa56f251601de73f80 Mon Sep 17 00:00:00 2001 From: SasmithaDilshan Date: Fri, 29 May 2026 14:09:29 +0530 Subject: [PATCH] Add Created By filter to All Cases view Adds a multi-select Created By dropdown filter to the All Cases page, populated from project contacts (same users as Settings > User Management). Includes staleTime alignment for contacts query and loading state sync. --- .../src/components/list-view/ListFilters.tsx | 26 ++++++++++++++++++- .../components/list-view/ListSearchPanel.tsx | 12 +++++++++ .../dashboard/utils/dashboardNavigation.ts | 6 +++++ .../settings/api/useGetProjectContacts.ts | 2 +- .../support/constants/supportConstants.ts | 5 ++++ .../features/support/pages/AllCasesPage.tsx | 10 ++++++- .../src/features/support/types/cases.ts | 2 ++ .../features/support/types/supportUiConfig.ts | 2 +- .../src/features/support/utils/support.ts | 3 +++ 9 files changed, 64 insertions(+), 4 deletions(-) diff --git a/apps/customer-portal/webapp/src/components/list-view/ListFilters.tsx b/apps/customer-portal/webapp/src/components/list-view/ListFilters.tsx index e1e1b3de18..25ca08e1c9 100644 --- a/apps/customer-portal/webapp/src/components/list-view/ListFilters.tsx +++ b/apps/customer-portal/webapp/src/components/list-view/ListFilters.tsx @@ -32,6 +32,7 @@ import type { AllCasesFilterValues, } from "@features/support/types/cases"; import type { ProjectDeploymentItem } from "@features/project-details/types/deployments"; +import type { ProjectContact } from "@features/settings/types/users"; import { SelectMenuLoadMoreRow, SELECT_MENU_LOAD_MORE_ROW_VALUE, @@ -49,6 +50,8 @@ export interface ListFiltersProps { filters: AllCasesFilterValues; filterMetadata: CaseMetadataResponse | undefined; deployments?: ProjectDeploymentItem[]; + contacts?: ProjectContact[]; + isContactsLoading?: boolean; onFilterChange: (field: string, value: string | string[]) => void; excludeS0?: boolean; restrictSeverityToLow?: boolean; @@ -56,6 +59,7 @@ export interface ListFiltersProps { hideStatusFilter?: boolean; hideDeploymentFilter?: boolean; hideCategoryFilter?: boolean; + hideCreatedByFilter?: boolean; onLoadMoreDeployments?: () => void; hasMoreDeployments?: boolean; isFetchingMoreDeployments?: boolean; @@ -72,6 +76,8 @@ export default function ListFilters({ filters, filterMetadata, deployments, + contacts, + isContactsLoading = false, onFilterChange, excludeS0 = false, restrictSeverityToLow = false, @@ -79,6 +85,7 @@ export default function ListFilters({ hideStatusFilter = false, hideDeploymentFilter = false, hideCategoryFilter = false, + hideCreatedByFilter = false, onLoadMoreDeployments, hasMoreDeployments = false, isFetchingMoreDeployments = false, @@ -112,12 +119,16 @@ export default function ListFilters({ if (hideCategoryFilter && def.id === "category") { return null; } + if (hideCreatedByFilter && def.id === "createdBy") { + return null; + } if (def.metadataKey === "severities" && restrictSeverityToLow) { return null; } const { label, allLabel } = deriveFilterLabels(def.id); const isDeploymentFilter = def.id === "deployment"; + const isCreatedByFilter = def.id === "createdBy"; const options = (() => { if (isDeploymentFilter) { return ( @@ -127,6 +138,15 @@ export default function ListFilters({ })) ?? [] ); } + if (isCreatedByFilter) { + return ( + contacts?.map((contact) => ({ + label: `${contact.firstName} ${contact.lastName}`.trim() || contact.email, + value: contact.email, + })) ?? [] + ); + } + if (!def.metadataKey) return []; const metadataOptions = filterMetadata?.[def.metadataKey]; if (!Array.isArray(metadataOptions)) return []; const filtered = @@ -189,7 +209,11 @@ export default function ListFilters({ ); }} > - {hasNoOptions ? ( + {isCreatedByFilter && isContactsLoading ? ( + + Loading... + + ) : hasNoOptions ? ( {EMPTY_DROPDOWN_PLACEHOLDER} diff --git a/apps/customer-portal/webapp/src/components/list-view/ListSearchPanel.tsx b/apps/customer-portal/webapp/src/components/list-view/ListSearchPanel.tsx index 6a62d71fab..1035b5d082 100644 --- a/apps/customer-portal/webapp/src/components/list-view/ListSearchPanel.tsx +++ b/apps/customer-portal/webapp/src/components/list-view/ListSearchPanel.tsx @@ -17,6 +17,7 @@ import type { JSX, ReactNode } from "react"; import type { CaseMetadataResponse } from "@features/support/types/cases"; import type { ProjectDeploymentItem } from "@features/project-details/types/deployments"; +import type { ProjectContact } from "@features/settings/types/users"; import ListSearchBar from "@components/list-view/ListSearchBar"; import ListFilters from "@components/list-view/ListFilters"; import { countListSearchAndFilters } from "@features/support/utils/support"; @@ -32,9 +33,12 @@ export interface ListSearchPanelProps { severityIds?: string[]; issueTypes?: string[]; deploymentIds?: string[]; + createdBy?: string[]; }; filterMetadata: CaseMetadataResponse | undefined; deployments?: ProjectDeploymentItem[]; + contacts?: ProjectContact[]; + isContactsLoading?: boolean; onFilterChange: (field: string, value: string | string[]) => void; onClearFilters: () => void; excludeS0?: boolean; @@ -43,6 +47,7 @@ export interface ListSearchPanelProps { hideStatusFilter?: boolean; hideDeploymentFilter?: boolean; hideCategoryFilter?: boolean; + hideCreatedByFilter?: boolean; hideFiltersButton?: boolean; isProjectContextLoading?: boolean; onLoadMoreDeployments?: () => void; @@ -67,6 +72,8 @@ export default function ListSearchPanel({ filters, filterMetadata, deployments, + contacts, + isContactsLoading = false, onFilterChange, onClearFilters, excludeS0 = false, @@ -75,6 +82,7 @@ export default function ListSearchPanel({ hideStatusFilter = false, hideDeploymentFilter = false, hideCategoryFilter = false, + hideCreatedByFilter = false, hideFiltersButton = false, isProjectContextLoading = false, onLoadMoreDeployments, @@ -90,6 +98,7 @@ export default function ListSearchPanel({ ...(hideStatusFilter ? { statusIds: undefined } : {}), ...(hideDeploymentFilter ? { deploymentIds: undefined } : {}), ...(hideCategoryFilter ? { issueTypes: undefined } : {}), + ...(hideCreatedByFilter ? { createdBy: undefined } : {}), ...excluded, }; return ( @@ -112,6 +121,8 @@ export default function ListSearchPanel({ filters={filters} filterMetadata={filterMetadata} deployments={deployments} + contacts={contacts} + isContactsLoading={isContactsLoading} onFilterChange={onFilterChange} excludeS0={excludeS0} restrictSeverityToLow={restrictSeverityToLow} @@ -119,6 +130,7 @@ export default function ListSearchPanel({ hideStatusFilter={hideStatusFilter} hideDeploymentFilter={hideDeploymentFilter} hideCategoryFilter={hideCategoryFilter} + hideCreatedByFilter={hideCreatedByFilter} onLoadMoreDeployments={onLoadMoreDeployments} hasMoreDeployments={hasMoreDeployments} isFetchingMoreDeployments={isFetchingMoreDeployments} diff --git a/apps/customer-portal/webapp/src/features/dashboard/utils/dashboardNavigation.ts b/apps/customer-portal/webapp/src/features/dashboard/utils/dashboardNavigation.ts index afdf1da2c3..7b6d8745fd 100644 --- a/apps/customer-portal/webapp/src/features/dashboard/utils/dashboardNavigation.ts +++ b/apps/customer-portal/webapp/src/features/dashboard/utils/dashboardNavigation.ts @@ -87,6 +87,7 @@ export function buildDashboardCaseSearchFilters(params: { deploymentIds?: string[]; searchQuery?: string; createdByMe?: boolean; + createdBy?: string[]; caseStates?: MetadataItem[]; isDashboardSeverityNavigation?: boolean; }): CaseSearchFilters { @@ -97,6 +98,7 @@ export function buildDashboardCaseSearchFilters(params: { deploymentIds, searchQuery, createdByMe, + createdBy, caseStates, isDashboardSeverityNavigation = false, } = params; @@ -107,6 +109,7 @@ export function buildDashboardCaseSearchFilters(params: { ? severityIds.map(Number) : undefined; const normalizedIssueIds = normalizeCaseSearchIssueIds(issueTypes); + const normalizedCreatedBy = createdBy?.length ? createdBy : undefined; switch (true) { case Boolean(explicitStatusIds?.length): @@ -117,6 +120,7 @@ export function buildDashboardCaseSearchFilters(params: { deploymentIds, searchQuery: normalizedSearchQuery, createdByMe, + createdBy: normalizedCreatedBy, }; case isDashboardSeverityNavigation: return { @@ -126,6 +130,7 @@ export function buildDashboardCaseSearchFilters(params: { deploymentIds, searchQuery: normalizedSearchQuery, createdByMe, + createdBy: normalizedCreatedBy, }; default: return { @@ -134,6 +139,7 @@ export function buildDashboardCaseSearchFilters(params: { deploymentIds, searchQuery: normalizedSearchQuery, createdByMe, + createdBy: normalizedCreatedBy, }; } } diff --git a/apps/customer-portal/webapp/src/features/settings/api/useGetProjectContacts.ts b/apps/customer-portal/webapp/src/features/settings/api/useGetProjectContacts.ts index f09a2f88a3..a2d2cf37af 100644 --- a/apps/customer-portal/webapp/src/features/settings/api/useGetProjectContacts.ts +++ b/apps/customer-portal/webapp/src/features/settings/api/useGetProjectContacts.ts @@ -70,6 +70,6 @@ export default function useGetProjectContacts( } }, enabled: !!projectId && !!isSignedIn && !isAuthLoading, - staleTime: 0, + staleTime: 5 * 60 * 1000, }); } diff --git a/apps/customer-portal/webapp/src/features/support/constants/supportConstants.ts b/apps/customer-portal/webapp/src/features/support/constants/supportConstants.ts index f7d0d007d5..e3212ffbbe 100644 --- a/apps/customer-portal/webapp/src/features/support/constants/supportConstants.ts +++ b/apps/customer-portal/webapp/src/features/support/constants/supportConstants.ts @@ -448,6 +448,11 @@ export const ALL_CASES_FILTER_DEFINITIONS: AllCasesFilterDefinition[] = [ metadataKey: "deploymentTypes", multiSelect: true, }, + { + filterKey: "createdBy", + id: "createdBy", + multiSelect: true, + }, ]; /** diff --git a/apps/customer-portal/webapp/src/features/support/pages/AllCasesPage.tsx b/apps/customer-portal/webapp/src/features/support/pages/AllCasesPage.tsx index 93f3010c60..0084ed0e71 100644 --- a/apps/customer-portal/webapp/src/features/support/pages/AllCasesPage.tsx +++ b/apps/customer-portal/webapp/src/features/support/pages/AllCasesPage.tsx @@ -35,6 +35,7 @@ import useGetProjectFeatures from "@api/useGetProjectFeatures"; import useGetProjectFilters from "@api/useGetProjectFilters"; import useGetProjectCases from "@api/useGetProjectCases"; import { usePostProjectDeploymentsSearchInfinite } from "@api/usePostProjectDeploymentsSearch"; +import useGetProjectContacts from "@features/settings/api/useGetProjectContacts"; import { hasListSearchOrFilters, getLast30DaysUtcRange, @@ -141,6 +142,9 @@ export default function AllCasesPage(): JSX.Element { const deploymentsList = deploymentsQuery.data?.pages.flatMap((p) => p.deployments ?? []) ?? []; + const { data: contactsData, isLoading: isContactsLoading } = useGetProjectContacts(projectId || ""); + const contactsList = contactsData ?? []; + const caseSearchRequest = useMemo( () => ({ filters: { @@ -154,6 +158,7 @@ export default function AllCasesPage(): JSX.Element { : undefined, searchQuery: searchTerm, createdByMe: createdByMe || undefined, + createdBy: filters.createdBy as string[] | undefined, caseStates: filterMetadata?.caseStates, isDashboardSeverityNavigation: (isDashboardSeverityNavigation && @@ -178,6 +183,7 @@ export default function AllCasesPage(): JSX.Element { filterMetadata?.caseStates, isDashboardSeverityNavigation, initialSeverityId, + contactsList, ], ); @@ -204,7 +210,7 @@ export default function AllCasesPage(): JSX.Element { (!!projectId && !hasCasesResponse) || isFetchingNextPage; - const isInitialPageLoading = isCasesAreaLoading; + const isInitialPageLoading = isCasesAreaLoading || isContactsLoading; useEffect(() => { if (isInitialPageLoading) { @@ -356,6 +362,8 @@ export default function AllCasesPage(): JSX.Element { onFiltersToggle={() => setIsFiltersOpen(!isFiltersOpen)} filters={filters} filterMetadata={filterMetadata} + contacts={contactsList} + isContactsLoading={isContactsLoading} deployments={ projectDetailsReady && permissions.hasDeployments ? deploymentsList diff --git a/apps/customer-portal/webapp/src/features/support/types/cases.ts b/apps/customer-portal/webapp/src/features/support/types/cases.ts index 8c206c7a8b..4b5baaac42 100644 --- a/apps/customer-portal/webapp/src/features/support/types/cases.ts +++ b/apps/customer-portal/webapp/src/features/support/types/cases.ts @@ -309,6 +309,7 @@ export type AllCasesFilterValues = { issueTypes?: string[]; deploymentIds?: string[]; engagementTypeKey?: string; + createdBy?: string[]; }; // Item type for a case attachment. @@ -367,6 +368,7 @@ export type CaseSearchFilters = { searchQuery?: string; caseTypes?: string[]; createdByMe?: boolean; + createdBy?: string[]; closedStartDate?: string; closedEndDate?: string; engagementTypeKeys?: number[]; diff --git a/apps/customer-portal/webapp/src/features/support/types/supportUiConfig.ts b/apps/customer-portal/webapp/src/features/support/types/supportUiConfig.ts index 611647c1d9..631ca53731 100644 --- a/apps/customer-portal/webapp/src/features/support/types/supportUiConfig.ts +++ b/apps/customer-portal/webapp/src/features/support/types/supportUiConfig.ts @@ -46,7 +46,7 @@ export type CaseStatusAction = { /** All-cases list filter field mapping. */ export type AllCasesFilterDefinition = { id: string; - metadataKey: keyof CaseMetadataResponse; + metadataKey?: keyof CaseMetadataResponse; filterKey: string; useLabelAsValue?: boolean; multiSelect?: boolean; diff --git a/apps/customer-portal/webapp/src/features/support/utils/support.ts b/apps/customer-portal/webapp/src/features/support/utils/support.ts index 5a11aa3d9e..34ad879121 100644 --- a/apps/customer-portal/webapp/src/features/support/utils/support.ts +++ b/apps/customer-portal/webapp/src/features/support/utils/support.ts @@ -791,6 +791,9 @@ export function deriveFilterLabels(id: string): { if (id === "caseType") { return { label: "Case Type", allLabel: "All Case Types" }; } + if (id === "createdBy") { + return { label: "Created By", allLabel: "All Users" }; + } const label = id.charAt(0).toUpperCase() + id.slice(1); const allLabel = `All ${ label.endsWith("s")