@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.0
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/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { FieldGroup } from "../ui/field";
|
|
2
|
+
import { StatusAlert } from "../alerts/status-alert";
|
|
3
|
+
import { CardLayout } from "../layout/card-layout";
|
|
4
|
+
import { SubmitButton } from "./submit-button";
|
|
5
|
+
import { Button } from "../ui/button";
|
|
6
|
+
import { useFormContext } from "../../hooks/form";
|
|
7
|
+
import { useId, useEffect, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
const SUCCESS_AUTO_DISMISS_DELAY = 3000;
|
|
10
|
+
|
|
11
|
+
interface FiltersFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
|
|
12
|
+
title: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
error?: React.ReactNode;
|
|
15
|
+
success?: React.ReactNode;
|
|
16
|
+
submit: {
|
|
17
|
+
text: string;
|
|
18
|
+
loadingText?: string;
|
|
19
|
+
};
|
|
20
|
+
reset?: {
|
|
21
|
+
text: string;
|
|
22
|
+
onReset: () => void;
|
|
23
|
+
};
|
|
24
|
+
onSuccessDismiss?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wrapper component that provides consistent layout and error/success alert positioning
|
|
29
|
+
* for all filter forms.
|
|
30
|
+
*/
|
|
31
|
+
export function FiltersForm({
|
|
32
|
+
id: providedId,
|
|
33
|
+
title,
|
|
34
|
+
description,
|
|
35
|
+
error,
|
|
36
|
+
success,
|
|
37
|
+
children,
|
|
38
|
+
submit,
|
|
39
|
+
reset,
|
|
40
|
+
onSuccessDismiss,
|
|
41
|
+
...props
|
|
42
|
+
}: FiltersFormProps) {
|
|
43
|
+
const form = useFormContext();
|
|
44
|
+
const generatedId = useId();
|
|
45
|
+
const id = providedId ?? generatedId;
|
|
46
|
+
|
|
47
|
+
const isSubmittingSelector = (state: { isSubmitting: boolean }) => state.isSubmitting;
|
|
48
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (timeoutRef.current) {
|
|
52
|
+
clearTimeout(timeoutRef.current);
|
|
53
|
+
timeoutRef.current = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (success && onSuccessDismiss) {
|
|
57
|
+
timeoutRef.current = setTimeout(() => {
|
|
58
|
+
onSuccessDismiss();
|
|
59
|
+
timeoutRef.current = null;
|
|
60
|
+
}, SUCCESS_AUTO_DISMISS_DELAY);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
if (timeoutRef.current) {
|
|
65
|
+
clearTimeout(timeoutRef.current);
|
|
66
|
+
timeoutRef.current = null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}, [success, onSuccessDismiss]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<CardLayout title={title} description={description}>
|
|
73
|
+
<div className="space-y-6">
|
|
74
|
+
{error && <StatusAlert variant="error">{error}</StatusAlert>}
|
|
75
|
+
{success && <StatusAlert variant="success">{success}</StatusAlert>}
|
|
76
|
+
|
|
77
|
+
<form
|
|
78
|
+
id={id}
|
|
79
|
+
onSubmit={(e) => {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
e.stopPropagation();
|
|
82
|
+
form.handleSubmit();
|
|
83
|
+
}}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
<FieldGroup>{children}</FieldGroup>
|
|
87
|
+
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
|
88
|
+
<SubmitButton
|
|
89
|
+
form={id}
|
|
90
|
+
label={submit.text}
|
|
91
|
+
loadingLabel={submit.loadingText}
|
|
92
|
+
className="flex-1"
|
|
93
|
+
/>
|
|
94
|
+
{reset && (
|
|
95
|
+
<form.Subscribe selector={isSubmittingSelector}>
|
|
96
|
+
{(isSubmitting: boolean) => (
|
|
97
|
+
<Button
|
|
98
|
+
type="button"
|
|
99
|
+
variant="outline"
|
|
100
|
+
onClick={reset.onReset}
|
|
101
|
+
className="flex-1"
|
|
102
|
+
disabled={isSubmitting}
|
|
103
|
+
>
|
|
104
|
+
{reset.text}
|
|
105
|
+
</Button>
|
|
106
|
+
)}
|
|
107
|
+
</form.Subscribe>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</form>
|
|
111
|
+
</div>
|
|
112
|
+
</CardLayout>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Button } from "../ui/button";
|
|
2
|
+
import { Spinner } from "../ui/spinner";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { useFormContext } from "../../hooks/form";
|
|
5
|
+
|
|
6
|
+
interface SubmitButtonProps extends Omit<React.ComponentProps<typeof Button>, "type" | "disabled"> {
|
|
7
|
+
/** Button text when not submitting */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Button text while submitting */
|
|
10
|
+
loadingLabel?: string;
|
|
11
|
+
/** Form id to associate with (for buttons outside form element) */
|
|
12
|
+
form?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const isSubmittingSelector = (state: { isSubmitting: boolean }) => state.isSubmitting;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Submit button that subscribes to form submission state.
|
|
19
|
+
* Disables interaction during submission and provides visual feedback.
|
|
20
|
+
*/
|
|
21
|
+
export function SubmitButton({
|
|
22
|
+
label,
|
|
23
|
+
loadingLabel = "Applying…",
|
|
24
|
+
className,
|
|
25
|
+
form: formId,
|
|
26
|
+
...props
|
|
27
|
+
}: SubmitButtonProps) {
|
|
28
|
+
const form = useFormContext();
|
|
29
|
+
return (
|
|
30
|
+
<form.Subscribe selector={isSubmittingSelector}>
|
|
31
|
+
{(isSubmitting: boolean) => (
|
|
32
|
+
<Button
|
|
33
|
+
type="submit"
|
|
34
|
+
form={formId}
|
|
35
|
+
className={cn("w-full", className)}
|
|
36
|
+
disabled={isSubmitting}
|
|
37
|
+
aria-label={isSubmitting ? loadingLabel : label}
|
|
38
|
+
aria-busy={isSubmitting}
|
|
39
|
+
{...props}
|
|
40
|
+
>
|
|
41
|
+
{isSubmitting && <Spinner className="mr-2" aria-hidden="true" />}
|
|
42
|
+
<span aria-live="polite">{isSubmitting ? loadingLabel : label}</span>
|
|
43
|
+
</Button>
|
|
44
|
+
)}
|
|
45
|
+
</form.Subscribe>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card";
|
|
2
|
+
|
|
3
|
+
interface CardLayoutProps {
|
|
4
|
+
title: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CardLayout({ title, description, children }: CardLayoutProps) {
|
|
10
|
+
return (
|
|
11
|
+
<Card className="w-full">
|
|
12
|
+
<CardHeader>
|
|
13
|
+
<CardTitle className="text-2xl">{title}</CardTitle>
|
|
14
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
15
|
+
</CardHeader>
|
|
16
|
+
<CardContent>{children}</CardContent>
|
|
17
|
+
</Card>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultCardFields Component
|
|
3
|
+
*
|
|
4
|
+
* Displays secondary fields (up to 3) for a search result card.
|
|
5
|
+
* Excludes the primary field and handles nested field values.
|
|
6
|
+
*
|
|
7
|
+
* @param record - The search result record data
|
|
8
|
+
* @param columns - Array of column definitions
|
|
9
|
+
* @param excludeFieldApiName - Field API name to exclude (usually the primary field)
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* - Displays up to 3 secondary fields
|
|
13
|
+
* - Handles nested field paths (e.g., "Owner.Alias")
|
|
14
|
+
* - Skips fields with null/undefined/empty values
|
|
15
|
+
* - Responsive layout (vertical on mobile, horizontal on desktop)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* <ResultCardFields
|
|
20
|
+
* record={searchResult}
|
|
21
|
+
* columns={columns}
|
|
22
|
+
* excludeFieldApiName="Name"
|
|
23
|
+
* />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
import type { Column, SearchResultRecordData } from "../../types/search/searchResults";
|
|
27
|
+
import { getNestedFieldValue } from "../../utils/fieldUtils";
|
|
28
|
+
|
|
29
|
+
interface ResultCardFieldsProps {
|
|
30
|
+
record: SearchResultRecordData;
|
|
31
|
+
columns: Column[];
|
|
32
|
+
excludeFieldApiName?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function ResultCardFields({
|
|
36
|
+
record,
|
|
37
|
+
columns,
|
|
38
|
+
excludeFieldApiName,
|
|
39
|
+
}: ResultCardFieldsProps) {
|
|
40
|
+
const secondaryFields = columns.filter(
|
|
41
|
+
(col) => col && col.fieldApiName && col.fieldApiName !== excludeFieldApiName,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<dl className="space-y-2" aria-label="Additional record information">
|
|
46
|
+
{secondaryFields.map((column) => {
|
|
47
|
+
if (!column || !column.fieldApiName) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const displayValue = getNestedFieldValue(record.fields, column.fieldApiName);
|
|
52
|
+
|
|
53
|
+
if (displayValue === null || displayValue === undefined || displayValue === "") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
key={column.fieldApiName}
|
|
60
|
+
className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"
|
|
61
|
+
>
|
|
62
|
+
<dt className="text-sm font-medium text-muted-foreground min-w-[100px]">
|
|
63
|
+
{column.label || column.fieldApiName}:
|
|
64
|
+
</dt>
|
|
65
|
+
<dd className="text-sm text-foreground">{displayValue}</dd>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</dl>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchHeader Component
|
|
3
|
+
*
|
|
4
|
+
* Displays the header for search results or browse-all (same UI).
|
|
5
|
+
* labelPlural comes from object metadata (e.g. useObjectInfoBatch) so it is not hard-coded.
|
|
6
|
+
*/
|
|
7
|
+
interface SearchHeaderProps {
|
|
8
|
+
query?: string;
|
|
9
|
+
isBrowseAll?: boolean;
|
|
10
|
+
/** Plural label for the primary object (e.g. "Accounts"). From object metadata. */
|
|
11
|
+
labelPlural?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function SearchHeader({
|
|
15
|
+
query,
|
|
16
|
+
isBrowseAll,
|
|
17
|
+
labelPlural = "records",
|
|
18
|
+
}: SearchHeaderProps) {
|
|
19
|
+
return (
|
|
20
|
+
<header className="mb-6" aria-label="Search results header">
|
|
21
|
+
<h1 className="text-3xl font-bold mb-2">
|
|
22
|
+
{isBrowseAll ? `Browse All ${labelPlural}` : "Search Results"}
|
|
23
|
+
</h1>
|
|
24
|
+
{!isBrowseAll && query && (
|
|
25
|
+
<p className="text-lg" aria-live="polite">
|
|
26
|
+
Results for: <span className="font-semibold">"{query}"</span>
|
|
27
|
+
</p>
|
|
28
|
+
)}
|
|
29
|
+
</header>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchPagination Component
|
|
3
|
+
*
|
|
4
|
+
* Displays pagination controls for search results.
|
|
5
|
+
* Previous/Next are disabled using hasPreviousPage/hasNextPage from the API so no request is made when there is no page.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* - Layout: page size selector on the left, prev/page/next controls on the right (corners).
|
|
9
|
+
* - Previous disabled when !hasPreviousPage (cursor stack enables prev when pageIndex > 0).
|
|
10
|
+
* - Next disabled when !hasNextPage or nextPageToken is null.
|
|
11
|
+
*/
|
|
12
|
+
import { Button } from "../ui/button";
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from "../ui/select";
|
|
20
|
+
import { Label } from "../ui/label";
|
|
21
|
+
import { PAGE_SIZE_OPTIONS, getValidPageSize, isValidPageSize } from "../../utils/paginationUtils";
|
|
22
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
interface SearchPaginationProps {
|
|
25
|
+
currentPageToken: string;
|
|
26
|
+
nextPageToken: string | null;
|
|
27
|
+
previousPageToken: string | null;
|
|
28
|
+
hasNextPage?: boolean;
|
|
29
|
+
hasPreviousPage?: boolean;
|
|
30
|
+
pageSize: number;
|
|
31
|
+
/** direction: 'prev' | 'next' | 'first'. When 'first' (e.g. page size change), parent typically resets pagination; token may be '' for prev when going to page 0. */
|
|
32
|
+
onPageChange: (newPageToken: string, direction?: "next" | "prev" | "first") => void;
|
|
33
|
+
onPageSizeChange: (newPageSize: number) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function SearchPagination({
|
|
37
|
+
currentPageToken,
|
|
38
|
+
nextPageToken,
|
|
39
|
+
previousPageToken,
|
|
40
|
+
hasNextPage = false,
|
|
41
|
+
hasPreviousPage = false,
|
|
42
|
+
pageSize,
|
|
43
|
+
onPageChange,
|
|
44
|
+
onPageSizeChange,
|
|
45
|
+
}: SearchPaginationProps) {
|
|
46
|
+
const validPageSize = getValidPageSize(pageSize);
|
|
47
|
+
|
|
48
|
+
const currentPageTokenNum = parseInt(currentPageToken, 10) || 0;
|
|
49
|
+
const currentPage = currentPageTokenNum + 1;
|
|
50
|
+
|
|
51
|
+
const canGoPrevious = Boolean(hasPreviousPage);
|
|
52
|
+
const canGoNext = Boolean(hasNextPage && nextPageToken != null);
|
|
53
|
+
|
|
54
|
+
const handlePrevious = () => {
|
|
55
|
+
if (canGoPrevious) {
|
|
56
|
+
onPageChange(previousPageToken ?? "", "prev");
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleNext = () => {
|
|
61
|
+
if (canGoNext && nextPageToken != null) {
|
|
62
|
+
onPageChange(nextPageToken, "next");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handlePageSizeChange = (newPageSize: string) => {
|
|
67
|
+
const newSize = parseInt(newPageSize, 10);
|
|
68
|
+
if (!isNaN(newSize) && isValidPageSize(newSize) && newSize !== validPageSize) {
|
|
69
|
+
onPageSizeChange(newSize);
|
|
70
|
+
onPageChange("0", "first");
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<nav
|
|
76
|
+
className="w-full flex flex-row flex-wrap items-center justify-between gap-4 py-2"
|
|
77
|
+
aria-label="Search results pagination"
|
|
78
|
+
>
|
|
79
|
+
<div
|
|
80
|
+
className="flex items-center gap-2 shrink-0"
|
|
81
|
+
role="group"
|
|
82
|
+
aria-label="Page size selector"
|
|
83
|
+
>
|
|
84
|
+
<Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
|
|
85
|
+
Results per page:
|
|
86
|
+
</Label>
|
|
87
|
+
<Select value={validPageSize.toString()} onValueChange={handlePageSizeChange}>
|
|
88
|
+
<SelectTrigger
|
|
89
|
+
id="page-size-select"
|
|
90
|
+
className="w-[70px]"
|
|
91
|
+
aria-label="Select number of results per page"
|
|
92
|
+
>
|
|
93
|
+
<SelectValue />
|
|
94
|
+
</SelectTrigger>
|
|
95
|
+
<SelectContent>
|
|
96
|
+
{PAGE_SIZE_OPTIONS.map((option) => (
|
|
97
|
+
<SelectItem key={option.value} value={option.value}>
|
|
98
|
+
{option.label}
|
|
99
|
+
</SelectItem>
|
|
100
|
+
))}
|
|
101
|
+
</SelectContent>
|
|
102
|
+
</Select>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="flex items-center gap-1 shrink-0" role="group" aria-label="Page navigation">
|
|
106
|
+
<Button
|
|
107
|
+
type="button"
|
|
108
|
+
variant="outline"
|
|
109
|
+
size="sm"
|
|
110
|
+
disabled={!canGoPrevious}
|
|
111
|
+
onClick={handlePrevious}
|
|
112
|
+
aria-label={
|
|
113
|
+
canGoPrevious
|
|
114
|
+
? `Go to previous page (Page ${currentPage - 1})`
|
|
115
|
+
: "Previous page (disabled)"
|
|
116
|
+
}
|
|
117
|
+
>
|
|
118
|
+
<ChevronLeft className="size-4" aria-hidden />
|
|
119
|
+
Previous
|
|
120
|
+
</Button>
|
|
121
|
+
<span
|
|
122
|
+
className="min-w-[4rem] text-center text-sm text-muted-foreground px-2"
|
|
123
|
+
aria-label={`Page ${currentPage}, current page`}
|
|
124
|
+
aria-current="page"
|
|
125
|
+
>
|
|
126
|
+
Page {currentPage}
|
|
127
|
+
</span>
|
|
128
|
+
<Button
|
|
129
|
+
type="button"
|
|
130
|
+
variant="outline"
|
|
131
|
+
size="sm"
|
|
132
|
+
disabled={!canGoNext}
|
|
133
|
+
onClick={handleNext}
|
|
134
|
+
aria-label={
|
|
135
|
+
canGoNext ? `Go to next page (Page ${currentPage + 1})` : "Next page (disabled)"
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
Next
|
|
139
|
+
<ChevronRight className="size-4" aria-hidden />
|
|
140
|
+
</Button>
|
|
141
|
+
</div>
|
|
142
|
+
</nav>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchResultsPanel Component
|
|
3
|
+
*
|
|
4
|
+
* Displays the search results panel with loading, error, and empty states.
|
|
5
|
+
* Renders a list of SearchResultCard components and pagination controls.
|
|
6
|
+
*
|
|
7
|
+
* @param columns - Array of column definitions for displaying result data
|
|
8
|
+
* @param results - Array of search result records to display
|
|
9
|
+
* @param columnsLoading - Whether column metadata is currently loading
|
|
10
|
+
* @param resultsLoading - Whether search results are currently loading
|
|
11
|
+
* @param columnsError - Error message if column fetch failed
|
|
12
|
+
* @param resultsError - Error message if results fetch failed
|
|
13
|
+
* @param currentPageToken - Current pagination token
|
|
14
|
+
* @param pageSize - Number of results per page
|
|
15
|
+
* @param onPageChange - Callback when pagination changes; second arg optional: "next" | "prev" | "first" (cursor-stack pagination)
|
|
16
|
+
* @param onPageSizeChange - Callback when page size changes
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <SearchResultsPanel
|
|
21
|
+
* columns={columns}
|
|
22
|
+
* results={results}
|
|
23
|
+
* columnsLoading={false}
|
|
24
|
+
* resultsLoading={false}
|
|
25
|
+
* columnsError={null}
|
|
26
|
+
* resultsError={null}
|
|
27
|
+
* currentPageToken="0"
|
|
28
|
+
* pageSize={25}
|
|
29
|
+
* onPageChange={(token, direction) => handlePageChange(token, direction)}
|
|
30
|
+
* onPageSizeChange={(size) => setPageSize(size)}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { useMemo } from "react";
|
|
35
|
+
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
|
36
|
+
import { Skeleton } from "../ui/skeleton";
|
|
37
|
+
import { AlertCircle } from "lucide-react";
|
|
38
|
+
import {
|
|
39
|
+
Select,
|
|
40
|
+
SelectContent,
|
|
41
|
+
SelectItem,
|
|
42
|
+
SelectTrigger,
|
|
43
|
+
SelectValue,
|
|
44
|
+
} from "../ui/select";
|
|
45
|
+
import { Label } from "../ui/label";
|
|
46
|
+
import SearchResultCard from "../SearchResultCard";
|
|
47
|
+
import SearchPagination from "./SearchPagination";
|
|
48
|
+
import type { Column, SearchResultRecord } from "../../types/search/searchResults";
|
|
49
|
+
import { getSafeKey } from "../../utils/recordUtils";
|
|
50
|
+
|
|
51
|
+
interface SearchResultsPanelProps {
|
|
52
|
+
/** API name of the object being searched (e.g. for detail page navigation). */
|
|
53
|
+
objectApiName?: string;
|
|
54
|
+
columns: Column[];
|
|
55
|
+
results: SearchResultRecord[];
|
|
56
|
+
columnsLoading: boolean;
|
|
57
|
+
resultsLoading: boolean;
|
|
58
|
+
columnsError: string | null;
|
|
59
|
+
resultsError: string | null;
|
|
60
|
+
currentPageToken: string;
|
|
61
|
+
nextPageToken: string | null;
|
|
62
|
+
previousPageToken: string | null;
|
|
63
|
+
hasNextPage?: boolean;
|
|
64
|
+
hasPreviousPage?: boolean;
|
|
65
|
+
pageSize: number;
|
|
66
|
+
sortBy: string;
|
|
67
|
+
onPageChange: (newPageToken: string, direction?: "next" | "prev" | "first") => void;
|
|
68
|
+
onPageSizeChange: (newPageSize: number) => void;
|
|
69
|
+
onSortByChange: (newSortBy: string) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default function SearchResultsPanel({
|
|
73
|
+
objectApiName,
|
|
74
|
+
columns,
|
|
75
|
+
results,
|
|
76
|
+
columnsLoading,
|
|
77
|
+
resultsLoading,
|
|
78
|
+
columnsError,
|
|
79
|
+
resultsError,
|
|
80
|
+
currentPageToken,
|
|
81
|
+
nextPageToken,
|
|
82
|
+
previousPageToken,
|
|
83
|
+
hasNextPage = false,
|
|
84
|
+
hasPreviousPage = false,
|
|
85
|
+
pageSize,
|
|
86
|
+
sortBy,
|
|
87
|
+
onPageChange,
|
|
88
|
+
onPageSizeChange,
|
|
89
|
+
onSortByChange,
|
|
90
|
+
}: SearchResultsPanelProps) {
|
|
91
|
+
const sortableColumns = useMemo(() => columns.filter(({ sortable }) => sortable), [columns]);
|
|
92
|
+
|
|
93
|
+
const validResults = useMemo(
|
|
94
|
+
() => results.filter((record) => record && record.record && record.record.id),
|
|
95
|
+
[results],
|
|
96
|
+
);
|
|
97
|
+
if (columnsError || resultsError) {
|
|
98
|
+
return (
|
|
99
|
+
<Alert variant="destructive" role="alert">
|
|
100
|
+
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
|
101
|
+
<AlertTitle>Error</AlertTitle>
|
|
102
|
+
<AlertDescription>
|
|
103
|
+
{columnsError || resultsError || "Failed to load search results"}
|
|
104
|
+
</AlertDescription>
|
|
105
|
+
</Alert>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (resultsLoading || columnsLoading) {
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
className="space-y-4"
|
|
113
|
+
role="status"
|
|
114
|
+
aria-live="polite"
|
|
115
|
+
aria-label="Loading search results"
|
|
116
|
+
>
|
|
117
|
+
<span className="sr-only">Loading search results</span>
|
|
118
|
+
{[1, 2, 3].map((i) => (
|
|
119
|
+
<div key={i} className="border rounded-lg p-6" aria-hidden="true">
|
|
120
|
+
<Skeleton className="h-6 w-3/4 mb-4" />
|
|
121
|
+
<Skeleton className="h-4 w-full mb-2" />
|
|
122
|
+
<Skeleton className="h-4 w-2/3" />
|
|
123
|
+
</div>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (results.length === 0) {
|
|
130
|
+
return (
|
|
131
|
+
<div className="text-center py-12" role="status" aria-live="polite">
|
|
132
|
+
<p className="text-lg mb-2">No results found</p>
|
|
133
|
+
<p className="text-sm">Try adjusting your search query or filters</p>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
{sortableColumns.length > 0 && (
|
|
141
|
+
<div
|
|
142
|
+
className="mb-6 flex items-center gap-2 justify-end"
|
|
143
|
+
role="group"
|
|
144
|
+
aria-label="Sort options"
|
|
145
|
+
>
|
|
146
|
+
<Label htmlFor="sort-by-select" className="text-sm font-normal whitespace-nowrap">
|
|
147
|
+
Sort by:
|
|
148
|
+
</Label>
|
|
149
|
+
<Select value={sortBy || ""} onValueChange={onSortByChange}>
|
|
150
|
+
<SelectTrigger
|
|
151
|
+
id="sort-by-select"
|
|
152
|
+
className="w-[200px]"
|
|
153
|
+
aria-label="Sort search results by field"
|
|
154
|
+
>
|
|
155
|
+
<SelectValue placeholder="Select field..." />
|
|
156
|
+
</SelectTrigger>
|
|
157
|
+
<SelectContent>
|
|
158
|
+
<SelectItem value="relevance">Relevance</SelectItem>
|
|
159
|
+
{sortableColumns.map((column) => (
|
|
160
|
+
<SelectItem key={column.fieldApiName} value={column.fieldApiName}>
|
|
161
|
+
{column.label}
|
|
162
|
+
</SelectItem>
|
|
163
|
+
))}
|
|
164
|
+
</SelectContent>
|
|
165
|
+
</Select>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<div className="space-y-4 mb-6" role="list" aria-label="Search results list">
|
|
170
|
+
{validResults.map((record, index) => {
|
|
171
|
+
const recordId = record.record.id;
|
|
172
|
+
const safeKey = getSafeKey(recordId, index);
|
|
173
|
+
return (
|
|
174
|
+
<div key={safeKey} role="listitem">
|
|
175
|
+
<SearchResultCard
|
|
176
|
+
record={record.record}
|
|
177
|
+
columns={columns}
|
|
178
|
+
objectApiName={objectApiName}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<SearchPagination
|
|
186
|
+
currentPageToken={currentPageToken}
|
|
187
|
+
nextPageToken={nextPageToken}
|
|
188
|
+
previousPageToken={previousPageToken}
|
|
189
|
+
hasNextPage={hasNextPage}
|
|
190
|
+
hasPreviousPage={hasPreviousPage}
|
|
191
|
+
pageSize={pageSize}
|
|
192
|
+
onPageChange={onPageChange}
|
|
193
|
+
onPageSizeChange={onPageSizeChange}
|
|
194
|
+
/>
|
|
195
|
+
</>
|
|
196
|
+
);
|
|
197
|
+
}
|