@salesforce/webapp-template-app-react-sample-b2e-experimental 1.72.0 → 1.73.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
- package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
- package/dist/package.json +1 -1
- package/package.json +5 -1
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import dashboardIcon from "../assets/icons/dashboard.svg";
|
|
|
4
4
|
import filesIcon from "../assets/icons/files.svg";
|
|
5
5
|
import propertiesIcon from "../assets/icons/properties.svg";
|
|
6
6
|
import maintenanceIcon from "../assets/icons/maintenance.svg";
|
|
7
|
+
import { PATHS } from "../lib/routeConfig.js";
|
|
7
8
|
|
|
8
9
|
interface NavItem {
|
|
9
10
|
path: string;
|
|
@@ -12,10 +13,11 @@ interface NavItem {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const navItems: NavItem[] = [
|
|
15
|
-
{ path:
|
|
16
|
-
{ path:
|
|
17
|
-
{ path:
|
|
18
|
-
{ path:
|
|
16
|
+
{ path: PATHS.HOME, icon: dashboardIcon, label: "Dashboard" },
|
|
17
|
+
{ path: PATHS.APPLICATIONS, icon: filesIcon, label: "Applications" },
|
|
18
|
+
{ path: PATHS.PROPERTIES, icon: propertiesIcon, label: "Properties" },
|
|
19
|
+
{ path: PATHS.MAINTENANCE_REQUESTS, icon: maintenanceIcon, label: "Maintenance Requests" },
|
|
20
|
+
{ path: PATHS.MAINTENANCE_WORKERS, icon: maintenanceIcon, label: "Maintenance Workers" },
|
|
19
21
|
];
|
|
20
22
|
|
|
21
23
|
export const VerticalNav: React.FC = () => {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ObjectInfoResult } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
2
|
+
import type { SearchableObjectConfig } from "../../lib/globalSearchConstants.js";
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@/components/ui/select";
|
|
10
|
+
|
|
11
|
+
type SearchableObjectApiName = SearchableObjectConfig["objectApiName"];
|
|
12
|
+
|
|
13
|
+
interface GlobalSearchBarProps {
|
|
14
|
+
objectApiNames: SearchableObjectApiName[];
|
|
15
|
+
objectInfos: (ObjectInfoResult | null)[];
|
|
16
|
+
searchableObjects: readonly SearchableObjectConfig[];
|
|
17
|
+
selectedObjectApiName: SearchableObjectApiName;
|
|
18
|
+
onSelectedObjectChange: (objectApiName: SearchableObjectApiName) => void;
|
|
19
|
+
searchQuery: string;
|
|
20
|
+
onSearchQueryChange: (value: string) => void;
|
|
21
|
+
onSearchSubmit: () => void;
|
|
22
|
+
onBrowseAll: () => void;
|
|
23
|
+
labelPlural: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Home page search: object dropdown + search input in a single combined control.
|
|
28
|
+
* Uses shadcn Select and object metadata for labels when available.
|
|
29
|
+
*/
|
|
30
|
+
export function GlobalSearchBar({
|
|
31
|
+
searchableObjects,
|
|
32
|
+
objectApiNames,
|
|
33
|
+
objectInfos,
|
|
34
|
+
selectedObjectApiName,
|
|
35
|
+
onSelectedObjectChange,
|
|
36
|
+
searchQuery,
|
|
37
|
+
onSearchQueryChange,
|
|
38
|
+
onSearchSubmit,
|
|
39
|
+
onBrowseAll,
|
|
40
|
+
labelPlural,
|
|
41
|
+
}: GlobalSearchBarProps) {
|
|
42
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
43
|
+
if (e.key === "Enter") {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
onSearchSubmit();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-wrap justify-end gap-2 items-center mb-4">
|
|
51
|
+
<div
|
|
52
|
+
className="w-full max-w-xl bg-white rounded-full px-4 py-3 shadow-sm border border-gray-200 flex items-center gap-2"
|
|
53
|
+
role="search"
|
|
54
|
+
aria-label={`Search ${labelPlural}`}
|
|
55
|
+
>
|
|
56
|
+
<Select
|
|
57
|
+
value={selectedObjectApiName}
|
|
58
|
+
onValueChange={(v) => onSelectedObjectChange(v as SearchableObjectApiName)}
|
|
59
|
+
>
|
|
60
|
+
<SelectTrigger
|
|
61
|
+
className="border-0 bg-transparent shadow-none focus:ring-0 focus:ring-offset-0 min-w-[120px] py-0 h-auto font-medium text-gray-700"
|
|
62
|
+
aria-label="Search in"
|
|
63
|
+
>
|
|
64
|
+
<SelectValue />
|
|
65
|
+
</SelectTrigger>
|
|
66
|
+
<SelectContent>
|
|
67
|
+
{searchableObjects.map((obj) => {
|
|
68
|
+
const idx = objectApiNames.indexOf(obj.objectApiName);
|
|
69
|
+
const info = idx >= 0 ? objectInfos[idx] : null;
|
|
70
|
+
const label =
|
|
71
|
+
(info?.labelPlural as string | undefined) ?? obj.fallbackLabelPlural ?? "Records";
|
|
72
|
+
return (
|
|
73
|
+
<SelectItem key={obj.objectApiName} value={obj.objectApiName}>
|
|
74
|
+
{label}
|
|
75
|
+
</SelectItem>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</SelectContent>
|
|
79
|
+
</Select>
|
|
80
|
+
<span className="text-gray-300 shrink-0" aria-hidden="true">
|
|
81
|
+
|
|
|
82
|
+
</span>
|
|
83
|
+
<input
|
|
84
|
+
type="search"
|
|
85
|
+
placeholder={`Search ${labelPlural}`}
|
|
86
|
+
value={searchQuery}
|
|
87
|
+
onChange={(e) => onSearchQueryChange(e.target.value)}
|
|
88
|
+
onKeyDown={handleKeyDown}
|
|
89
|
+
className="flex-1 outline-none text-gray-600 bg-transparent min-w-0"
|
|
90
|
+
aria-label={`Search ${labelPlural}`}
|
|
91
|
+
/>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
onClick={onSearchSubmit}
|
|
95
|
+
disabled={!searchQuery.trim()}
|
|
96
|
+
className="p-1 rounded-full hover:bg-gray-100 disabled:opacity-50 disabled:pointer-events-none shrink-0"
|
|
97
|
+
aria-label="Submit search"
|
|
98
|
+
>
|
|
99
|
+
<svg
|
|
100
|
+
className="w-5 h-5 text-gray-500"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
viewBox="0 0 24 24"
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
>
|
|
106
|
+
<path
|
|
107
|
+
strokeLinecap="round"
|
|
108
|
+
strokeLinejoin="round"
|
|
109
|
+
strokeWidth={2}
|
|
110
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
111
|
+
/>
|
|
112
|
+
</svg>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={onBrowseAll}
|
|
118
|
+
className="px-4 py-3 text-sm font-medium text-[#372949] bg-white border border-gray-200 rounded-full shadow-sm hover:bg-gray-50 whitespace-nowrap"
|
|
119
|
+
aria-label={`Browse all ${labelPlural}`}
|
|
120
|
+
>
|
|
121
|
+
Browse all {labelPlural}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface FilterErrorAlertProps {
|
|
2
|
+
message: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** Inline alert for filter validation errors (e.g. range min > max). Announced to screen readers. */
|
|
6
|
+
export function FilterErrorAlert({ message }: FilterErrorAlertProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
role="alert"
|
|
10
|
+
className="text-sm text-red-600 bg-red-50 border border-red-200 rounded-md px-4 py-2"
|
|
11
|
+
>
|
|
12
|
+
{message}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface PageErrorStateProps {
|
|
2
|
+
message: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Full-page error state when list or metadata fails to load.
|
|
7
|
+
* Uses role="alert" so screen readers announce the error.
|
|
8
|
+
*/
|
|
9
|
+
export function PageErrorState({ message }: PageErrorStateProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
className="flex justify-center items-center min-h-[200px] bg-gray-50"
|
|
13
|
+
role="alert"
|
|
14
|
+
aria-live="assertive"
|
|
15
|
+
>
|
|
16
|
+
<p className="text-red-600">{message}</p>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface PageLoadingStateProps {
|
|
2
|
+
message?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Full-page loading state. Uses aria-live="polite" so screen readers can announce updates.
|
|
7
|
+
*/
|
|
8
|
+
export function PageLoadingState({ message = "Loading..." }: PageLoadingStateProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className="flex justify-center items-center min-h-[200px] bg-gray-50"
|
|
12
|
+
aria-live="polite"
|
|
13
|
+
aria-busy="true"
|
|
14
|
+
>
|
|
15
|
+
<p className="text-gray-600">{message}</p>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Filter } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
2
|
+
import { getRangeMinKey, getRangeMaxKey } from "../../lib/filterUtils.js";
|
|
3
|
+
|
|
4
|
+
const inputClass =
|
|
5
|
+
"h-9 px-3 rounded-md border border-gray-300 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent w-20";
|
|
6
|
+
|
|
7
|
+
interface FilterFieldRangeProps {
|
|
8
|
+
filter: Filter;
|
|
9
|
+
formValues: Record<string, string>;
|
|
10
|
+
onChange: (key: string, value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FilterFieldRange({ filter, formValues, onChange }: FilterFieldRangeProps) {
|
|
14
|
+
const label = filter.label || filter.targetFieldPath;
|
|
15
|
+
const minKey = getRangeMinKey(filter.targetFieldPath);
|
|
16
|
+
const maxKey = getRangeMaxKey(filter.targetFieldPath);
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-col gap-1.5 min-w-[140px]" role="group" aria-label={label + " range"}>
|
|
19
|
+
<label className="text-sm font-medium text-gray-700 whitespace-nowrap">{label}</label>
|
|
20
|
+
<div className="flex gap-2">
|
|
21
|
+
<input
|
|
22
|
+
type="text"
|
|
23
|
+
value={formValues[minKey] ?? ""}
|
|
24
|
+
onChange={(e) => onChange(minKey, e.target.value)}
|
|
25
|
+
placeholder="Min"
|
|
26
|
+
className={inputClass}
|
|
27
|
+
aria-label={label + " minimum"}
|
|
28
|
+
/>
|
|
29
|
+
<input
|
|
30
|
+
type="text"
|
|
31
|
+
value={formValues[maxKey] ?? ""}
|
|
32
|
+
onChange={(e) => onChange(maxKey, e.target.value)}
|
|
33
|
+
placeholder="Max"
|
|
34
|
+
className={inputClass}
|
|
35
|
+
aria-label={label + " maximum"}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { Filter } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
3
|
+
import { ALL_PLACEHOLDER_VALUE, MULTI_VALUE_SEP } from "../../lib/filterUtils.js";
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from "@/components/ui/select";
|
|
11
|
+
|
|
12
|
+
const TRIGGER_CLASS =
|
|
13
|
+
"h-9 rounded-md border border-gray-300 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent min-w-[140px] data-[size=default]:h-9";
|
|
14
|
+
|
|
15
|
+
interface FilterFieldSelectProps {
|
|
16
|
+
filter: Filter;
|
|
17
|
+
options: { label?: string; value: string }[];
|
|
18
|
+
value: string;
|
|
19
|
+
onChange: (value: string) => void;
|
|
20
|
+
multiSelect?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function FilterFieldSelectSingle({
|
|
24
|
+
filter,
|
|
25
|
+
options,
|
|
26
|
+
value,
|
|
27
|
+
onChange,
|
|
28
|
+
}: Omit<FilterFieldSelectProps, "multiSelect">) {
|
|
29
|
+
const label = filter.label || filter.targetFieldPath;
|
|
30
|
+
const placeholder = filter.attributes?.placeholder || "All";
|
|
31
|
+
const id = `filter-${filter.targetFieldPath}`;
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col gap-1.5 min-w-[160px]" role="group" aria-label={label}>
|
|
34
|
+
<label htmlFor={id} className="text-sm font-medium text-gray-700 whitespace-nowrap">
|
|
35
|
+
{label}
|
|
36
|
+
</label>
|
|
37
|
+
<Select
|
|
38
|
+
value={value || ALL_PLACEHOLDER_VALUE}
|
|
39
|
+
onValueChange={(v) => onChange(v === ALL_PLACEHOLDER_VALUE ? "" : (v ?? ""))}
|
|
40
|
+
>
|
|
41
|
+
<SelectTrigger id={id} className={TRIGGER_CLASS} size="default" aria-label={label}>
|
|
42
|
+
<SelectValue placeholder={placeholder} />
|
|
43
|
+
</SelectTrigger>
|
|
44
|
+
<SelectContent>
|
|
45
|
+
<SelectItem value={ALL_PLACEHOLDER_VALUE}>{placeholder}</SelectItem>
|
|
46
|
+
{options.map((opt) => (
|
|
47
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
48
|
+
{opt.label || opt.value}
|
|
49
|
+
</SelectItem>
|
|
50
|
+
))}
|
|
51
|
+
</SelectContent>
|
|
52
|
+
</Select>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function FilterFieldMultiSelect({
|
|
58
|
+
filter,
|
|
59
|
+
options,
|
|
60
|
+
value,
|
|
61
|
+
onChange,
|
|
62
|
+
}: Omit<FilterFieldSelectProps, "multiSelect">) {
|
|
63
|
+
const label = filter.label || filter.targetFieldPath;
|
|
64
|
+
const selected = value ? value.split(MULTI_VALUE_SEP).filter(Boolean) : [];
|
|
65
|
+
const [open, setOpen] = React.useState(false);
|
|
66
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
|
67
|
+
const panelRef = React.useRef<HTMLDivElement>(null);
|
|
68
|
+
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (!open) return;
|
|
71
|
+
const close = (e: MouseEvent) => {
|
|
72
|
+
if (
|
|
73
|
+
triggerRef.current?.contains(e.target as Node) ||
|
|
74
|
+
panelRef.current?.contains(e.target as Node)
|
|
75
|
+
)
|
|
76
|
+
return;
|
|
77
|
+
setOpen(false);
|
|
78
|
+
};
|
|
79
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
80
|
+
if (e.key === "Escape") setOpen(false);
|
|
81
|
+
};
|
|
82
|
+
document.addEventListener("mousedown", close);
|
|
83
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
84
|
+
return () => {
|
|
85
|
+
document.removeEventListener("mousedown", close);
|
|
86
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
87
|
+
};
|
|
88
|
+
}, [open]);
|
|
89
|
+
|
|
90
|
+
const toggle = (optValue: string) => {
|
|
91
|
+
const next = selected.includes(optValue)
|
|
92
|
+
? selected.filter((v) => v !== optValue)
|
|
93
|
+
: [...selected, optValue];
|
|
94
|
+
onChange(next.join(MULTI_VALUE_SEP));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const displayText =
|
|
98
|
+
selected.length === 0
|
|
99
|
+
? filter.attributes?.placeholder || "All"
|
|
100
|
+
: selected.length === 1
|
|
101
|
+
? options.find((o) => o.value === selected[0])?.label || selected[0]
|
|
102
|
+
: `${selected.length} selected`;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="flex flex-col gap-1.5 min-w-[160px]" role="group" aria-label={label}>
|
|
106
|
+
<label className="text-sm font-medium text-gray-700 whitespace-nowrap">{label}</label>
|
|
107
|
+
<div className="relative">
|
|
108
|
+
<button
|
|
109
|
+
ref={triggerRef}
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => setOpen((o) => !o)}
|
|
112
|
+
className={`${TRIGGER_CLASS} w-full flex items-center justify-between gap-2 px-3 text-left`}
|
|
113
|
+
aria-label={label}
|
|
114
|
+
aria-expanded={open}
|
|
115
|
+
aria-haspopup="listbox"
|
|
116
|
+
>
|
|
117
|
+
<span className="truncate">{displayText}</span>
|
|
118
|
+
<svg
|
|
119
|
+
className={`h-4 w-4 shrink-0 text-gray-500 transition-transform ${open ? "rotate-180" : ""}`}
|
|
120
|
+
fill="none"
|
|
121
|
+
stroke="currentColor"
|
|
122
|
+
viewBox="0 0 24 24"
|
|
123
|
+
aria-hidden
|
|
124
|
+
>
|
|
125
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
126
|
+
</svg>
|
|
127
|
+
</button>
|
|
128
|
+
{open && (
|
|
129
|
+
<div
|
|
130
|
+
ref={panelRef}
|
|
131
|
+
role="listbox"
|
|
132
|
+
className="absolute top-full left-0 z-50 mt-1 min-w-[var(--radix-select-trigger-width)] rounded-lg border border-gray-200 bg-white py-1 shadow-md max-h-60 overflow-auto"
|
|
133
|
+
>
|
|
134
|
+
{options.map((opt) => {
|
|
135
|
+
const isSelected = selected.includes(opt.value);
|
|
136
|
+
return (
|
|
137
|
+
<button
|
|
138
|
+
key={opt.value}
|
|
139
|
+
type="button"
|
|
140
|
+
role="option"
|
|
141
|
+
aria-selected={isSelected}
|
|
142
|
+
onClick={() => toggle(opt.value)}
|
|
143
|
+
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
|
|
144
|
+
>
|
|
145
|
+
<span
|
|
146
|
+
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded border ${
|
|
147
|
+
isSelected ? "bg-purple-600 border-purple-600" : "border-gray-300"
|
|
148
|
+
}`}
|
|
149
|
+
>
|
|
150
|
+
{isSelected && (
|
|
151
|
+
<svg
|
|
152
|
+
className="h-3 w-3 text-white"
|
|
153
|
+
viewBox="0 0 12 12"
|
|
154
|
+
fill="none"
|
|
155
|
+
stroke="currentColor"
|
|
156
|
+
strokeWidth="2"
|
|
157
|
+
strokeLinecap="round"
|
|
158
|
+
strokeLinejoin="round"
|
|
159
|
+
>
|
|
160
|
+
<polyline points="2,6 5,9 10,3" />
|
|
161
|
+
</svg>
|
|
162
|
+
)}
|
|
163
|
+
</span>
|
|
164
|
+
{opt.label || opt.value}
|
|
165
|
+
</button>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function FilterFieldSelect({
|
|
176
|
+
filter,
|
|
177
|
+
options,
|
|
178
|
+
value,
|
|
179
|
+
onChange,
|
|
180
|
+
multiSelect = true,
|
|
181
|
+
}: FilterFieldSelectProps) {
|
|
182
|
+
if (multiSelect) {
|
|
183
|
+
return (
|
|
184
|
+
<FilterFieldMultiSelect filter={filter} options={options} value={value} onChange={onChange} />
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return (
|
|
188
|
+
<FilterFieldSelectSingle filter={filter} options={options} value={value} onChange={onChange} />
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Filter } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
2
|
+
|
|
3
|
+
const INPUT_CLASS =
|
|
4
|
+
"h-9 px-3 rounded-md border border-gray-300 bg-white text-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent min-w-[140px]";
|
|
5
|
+
|
|
6
|
+
interface FilterFieldTextProps {
|
|
7
|
+
filter: Filter;
|
|
8
|
+
value: string;
|
|
9
|
+
onChange: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FilterFieldText({ filter, value, onChange }: FilterFieldTextProps) {
|
|
13
|
+
const label = filter.label || filter.targetFieldPath;
|
|
14
|
+
const id = `filter-${filter.targetFieldPath}`;
|
|
15
|
+
const placeholder = filter.attributes?.placeholder || `Enter ${label.toLowerCase()}`;
|
|
16
|
+
return (
|
|
17
|
+
<div className="flex flex-col gap-1.5 min-w-[160px]">
|
|
18
|
+
<label htmlFor={id} className="text-sm font-medium text-gray-700 whitespace-nowrap">
|
|
19
|
+
{label}
|
|
20
|
+
</label>
|
|
21
|
+
<input
|
|
22
|
+
id={id}
|
|
23
|
+
type="text"
|
|
24
|
+
value={value}
|
|
25
|
+
onChange={(e) => onChange(e.target.value)}
|
|
26
|
+
placeholder={placeholder}
|
|
27
|
+
className={INPUT_CLASS}
|
|
28
|
+
aria-label={label}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Filter } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
2
|
+
import { FilterFieldRange } from "./FilterFieldRange.js";
|
|
3
|
+
import { FilterFieldSelect } from "./FilterFieldSelect.js";
|
|
4
|
+
import { FilterFieldText } from "./FilterFieldText.js";
|
|
5
|
+
|
|
6
|
+
/** Compatible with feature picklist option shape (label/value). */
|
|
7
|
+
interface PicklistOption {
|
|
8
|
+
label?: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
interface ListPageFilterRowProps {
|
|
12
|
+
filters: Filter[];
|
|
13
|
+
picklistValues: Record<string, PicklistOption[]>;
|
|
14
|
+
formValues: Record<string, string>;
|
|
15
|
+
onFormValueChange: (key: string, value: string) => void;
|
|
16
|
+
onApply: () => void;
|
|
17
|
+
onReset: () => void;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Horizontal row of filter controls: range, select (multi-select), and text fields + Apply/Reset.
|
|
23
|
+
*/
|
|
24
|
+
export function ListPageFilterRow({
|
|
25
|
+
filters,
|
|
26
|
+
picklistValues,
|
|
27
|
+
formValues,
|
|
28
|
+
onFormValueChange,
|
|
29
|
+
onApply,
|
|
30
|
+
onReset,
|
|
31
|
+
ariaLabel = "Filters",
|
|
32
|
+
}: ListPageFilterRowProps) {
|
|
33
|
+
if (filters.length === 0) return null;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
className="bg-white rounded-lg shadow-sm border border-gray-200 p-4"
|
|
38
|
+
role="region"
|
|
39
|
+
aria-label={ariaLabel}
|
|
40
|
+
>
|
|
41
|
+
<div className="flex flex-wrap items-end gap-4">
|
|
42
|
+
{filters.map((filter) => {
|
|
43
|
+
if (!filter?.targetFieldPath) return null;
|
|
44
|
+
const affordance = (filter.affordance ?? "").toLowerCase();
|
|
45
|
+
|
|
46
|
+
if (affordance === "range") {
|
|
47
|
+
return (
|
|
48
|
+
<FilterFieldRange
|
|
49
|
+
key={filter.targetFieldPath}
|
|
50
|
+
filter={filter}
|
|
51
|
+
formValues={formValues}
|
|
52
|
+
onChange={onFormValueChange}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (affordance === "select") {
|
|
58
|
+
const options = picklistValues[filter.targetFieldPath] ?? [];
|
|
59
|
+
const value = formValues[filter.targetFieldPath] ?? "";
|
|
60
|
+
return (
|
|
61
|
+
<FilterFieldSelect
|
|
62
|
+
key={filter.targetFieldPath}
|
|
63
|
+
filter={filter}
|
|
64
|
+
options={options}
|
|
65
|
+
value={value}
|
|
66
|
+
onChange={(v) => onFormValueChange(filter.targetFieldPath, v)}
|
|
67
|
+
multiSelect={true}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<FilterFieldText
|
|
74
|
+
key={filter.targetFieldPath}
|
|
75
|
+
filter={filter}
|
|
76
|
+
value={formValues[filter.targetFieldPath] ?? ""}
|
|
77
|
+
onChange={(v) => onFormValueChange(filter.targetFieldPath, v)}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
})}
|
|
81
|
+
<div className="flex items-center gap-2 ml-2 shrink-0">
|
|
82
|
+
<button
|
|
83
|
+
onClick={onApply}
|
|
84
|
+
className="h-9 px-4 bg-purple-700 hover:bg-purple-800 text-white text-sm font-medium rounded-md"
|
|
85
|
+
aria-label="Apply filters"
|
|
86
|
+
>
|
|
87
|
+
Apply
|
|
88
|
+
</button>
|
|
89
|
+
<button
|
|
90
|
+
onClick={onReset}
|
|
91
|
+
className="h-9 px-4 text-sm font-medium rounded-md border-gray-300"
|
|
92
|
+
aria-label="Reset filters"
|
|
93
|
+
>
|
|
94
|
+
Reset
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface PageHeaderProps {
|
|
2
|
+
title: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Page title and optional description. Uses a consistent wrapper (max-w-7xl mx-auto px-8 pt-8)
|
|
8
|
+
* so the header aligns with list/content on all pages.
|
|
9
|
+
*/
|
|
10
|
+
export function PageHeader({ title, description }: PageHeaderProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="max-w-7xl mx-auto px-8 pt-8">
|
|
13
|
+
<div className="mb-6">
|
|
14
|
+
<h1 className="text-2xl font-bold text-gray-900">{title}</h1>
|
|
15
|
+
{description != null && description !== "" && (
|
|
16
|
+
<p className="text-gray-600 mt-1">{description}</p>
|
|
17
|
+
)}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { Filter } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
3
|
+
import { PageContainer } from "../layout/PageContainer.js";
|
|
4
|
+
import { PageLoadingState } from "../feedback/PageLoadingState.js";
|
|
5
|
+
import { PageErrorState } from "../feedback/PageErrorState.js";
|
|
6
|
+
import { FilterErrorAlert } from "../feedback/FilterErrorAlert.js";
|
|
7
|
+
import { ListPageFilterRow } from "../filters/ListPageFilterRow.js";
|
|
8
|
+
|
|
9
|
+
/** Compatible with feature picklist option shape. */
|
|
10
|
+
interface PicklistOption {
|
|
11
|
+
label?: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ListPageWithFiltersProps {
|
|
16
|
+
filterProps: {
|
|
17
|
+
filters: Filter[];
|
|
18
|
+
picklistValues: Record<string, PicklistOption[]>;
|
|
19
|
+
formValues: Record<string, string>;
|
|
20
|
+
onFormValueChange: (key: string, value: string) => void;
|
|
21
|
+
onApply: () => void;
|
|
22
|
+
onReset: () => void;
|
|
23
|
+
ariaLabel: string;
|
|
24
|
+
};
|
|
25
|
+
filterError: string | null;
|
|
26
|
+
loading: boolean;
|
|
27
|
+
error: string | null;
|
|
28
|
+
loadingMessage: string;
|
|
29
|
+
isEmpty: boolean;
|
|
30
|
+
/** When set, a search input is shown that syncs with URL ?q= and drives list search. */
|
|
31
|
+
searchPlaceholder?: string;
|
|
32
|
+
searchAriaLabel?: string;
|
|
33
|
+
children: ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ListPageWithFilters({
|
|
37
|
+
filterProps,
|
|
38
|
+
filterError,
|
|
39
|
+
loading,
|
|
40
|
+
error,
|
|
41
|
+
loadingMessage,
|
|
42
|
+
isEmpty,
|
|
43
|
+
children,
|
|
44
|
+
}: ListPageWithFiltersProps) {
|
|
45
|
+
if (error) {
|
|
46
|
+
return <PageErrorState message={error} />;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (loading && isEmpty) {
|
|
50
|
+
return <PageLoadingState message={loadingMessage} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<PageContainer>
|
|
55
|
+
<div className="max-w-7xl mx-auto space-y-6">
|
|
56
|
+
<ListPageFilterRow
|
|
57
|
+
filters={filterProps.filters}
|
|
58
|
+
picklistValues={filterProps.picklistValues}
|
|
59
|
+
formValues={filterProps.formValues}
|
|
60
|
+
onFormValueChange={filterProps.onFormValueChange}
|
|
61
|
+
onApply={filterProps.onApply}
|
|
62
|
+
onReset={filterProps.onReset}
|
|
63
|
+
ariaLabel={filterProps.ariaLabel}
|
|
64
|
+
/>
|
|
65
|
+
{filterError && <FilterErrorAlert message={filterError} />}
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
</PageContainer>
|
|
69
|
+
);
|
|
70
|
+
}
|