@salesforce/ui-bundle-template-feature-react-search 11.3.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/LICENSE.txt +82 -0
- package/README.md +692 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/CHANGELOG.md +3499 -0
- package/dist/README.md +28 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/eslint.config.js +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.forceignore +15 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.graphqlrc.yml +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierignore +9 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierrc +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/CHANGELOG.md +10 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/README.md +75 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/codegen.yml +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/components.json +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/e2e/app.spec.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/eslint.config.js +169 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/feature-react-search.uibundle-meta.xml +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/index.html +12 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/package.json +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/playwright.config.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/get-graphql-schema.mjs +71 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/rewrite-e2e-assets.mjs +23 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/api/graphqlClient.ts +44 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/app.tsx +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/appLayout.tsx +83 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/alerts/status-alert.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/layouts/card-layout.tsx +29 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/alert.tsx +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/avatar.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/badge.tsx +48 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/breadcrumb.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/button.tsx +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/calendar.tsx +232 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/card.tsx +103 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/checkbox.tsx +32 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/collapsible.tsx +33 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/datePicker.tsx +127 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dialog.tsx +162 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dropdown-menu.tsx +257 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/field.tsx +237 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/index.ts +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/label.tsx +22 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/pagination.tsx +132 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/popover.tsx +89 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/select.tsx +193 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/skeleton.tsx +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/sonner.tsx +20 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/spinner.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/tabs.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/hooks/useAsyncData.ts +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/navigationMenu.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/router-utils.tsx +35 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +22 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/styles/global.css +135 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.json +45 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/ui-bundle.json +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vite-env.d.ts +4 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vite.config.ts +106 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.config.ts +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.setup.ts +1 -0
- package/dist/jest.config.js +6 -0
- package/dist/package-lock.json +9995 -0
- package/dist/package.json +44 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/gitignore-templates.json +4 -0
- package/dist/scripts/graphql-search.sh +191 -0
- package/dist/scripts/org-setup-config-schema.mjs +96 -0
- package/dist/scripts/org-setup.config.json +5 -0
- package/dist/scripts/org-setup.mjs +1392 -0
- package/dist/scripts/sf-project-setup.mjs +103 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/scripts/validate-org-setup-config.mjs +38 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +51 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/__inherit__appLayout.tsx +9 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__alert.tsx +39 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__button.tsx +45 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__checkbox.tsx +8 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__input.tsx +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__label.tsx +8 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__pagination.tsx +47 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__select.tsx +57 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__skeleton.tsx +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +10 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Select,
|
|
3
|
+
SelectContent,
|
|
4
|
+
SelectItem,
|
|
5
|
+
SelectTrigger,
|
|
6
|
+
SelectValue,
|
|
7
|
+
} from "../../../../../components/ui/select";
|
|
8
|
+
import { useFilterField } from "../FilterContext";
|
|
9
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
10
|
+
|
|
11
|
+
const ALL_VALUE = "__all__";
|
|
12
|
+
|
|
13
|
+
interface SelectFilterProps {
|
|
14
|
+
field: string;
|
|
15
|
+
label: string;
|
|
16
|
+
options: Array<{ value: string; label: string }>;
|
|
17
|
+
helpText?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SelectFilter({ field, label, options, helpText }: SelectFilterProps) {
|
|
21
|
+
const { value, onChange } = useFilterField(field);
|
|
22
|
+
const id = `filter-${field}`;
|
|
23
|
+
return (
|
|
24
|
+
<FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
|
|
25
|
+
<Select
|
|
26
|
+
value={value?.value ?? ALL_VALUE}
|
|
27
|
+
onValueChange={(v) => {
|
|
28
|
+
if (v === ALL_VALUE) onChange(undefined);
|
|
29
|
+
else onChange({ field, label, type: "picklist", value: v });
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<SelectTrigger id={id} className="w-full">
|
|
33
|
+
<SelectValue placeholder={`Select ${label.toLowerCase()}`} />
|
|
34
|
+
</SelectTrigger>
|
|
35
|
+
<SelectContent>
|
|
36
|
+
<SelectItem value={ALL_VALUE}>All</SelectItem>
|
|
37
|
+
{options.map((opt) => (
|
|
38
|
+
<SelectItem key={opt.value} value={opt.value}>
|
|
39
|
+
{opt.label}
|
|
40
|
+
</SelectItem>
|
|
41
|
+
))}
|
|
42
|
+
</SelectContent>
|
|
43
|
+
</Select>
|
|
44
|
+
</FilterFieldWrapper>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Input } from "../../../../../components/ui/input";
|
|
3
|
+
import { useFilterField } from "../FilterContext";
|
|
4
|
+
import { FilterFieldWrapper } from "./FilterFieldWrapper";
|
|
5
|
+
import { debounce, FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
|
|
6
|
+
|
|
7
|
+
interface TextFilterProps {
|
|
8
|
+
field: string;
|
|
9
|
+
label: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
helpText?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TextFilter({ field, label, placeholder, helpText }: TextFilterProps) {
|
|
15
|
+
const { value, onChange } = useFilterField(field);
|
|
16
|
+
const [draft, setDraft] = useState(value?.value ?? "");
|
|
17
|
+
|
|
18
|
+
const onChangeRef = useRef(onChange);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
onChangeRef.current = onChange;
|
|
21
|
+
});
|
|
22
|
+
// Build the debounced caller in a mount effect, not during render: creating it
|
|
23
|
+
// inline would read onChangeRef.current at render time (react-hooks/refs). It
|
|
24
|
+
// stays a single stable instance and always calls the latest onChange via the ref.
|
|
25
|
+
const debouncedRef = useRef<((next: string) => void) | undefined>(undefined);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
debouncedRef.current = debounce((next: string) => {
|
|
28
|
+
if (next === "") onChangeRef.current(undefined);
|
|
29
|
+
else onChangeRef.current({ field, label, type: "text", value: next });
|
|
30
|
+
}, FILTER_DEBOUNCE_MS);
|
|
31
|
+
}, [field, label]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setDraft(value?.value ?? "");
|
|
35
|
+
}, [value?.value]);
|
|
36
|
+
|
|
37
|
+
const id = `filter-${field}`;
|
|
38
|
+
return (
|
|
39
|
+
<FilterFieldWrapper label={label} htmlFor={id} helpText={helpText}>
|
|
40
|
+
<Input
|
|
41
|
+
id={id}
|
|
42
|
+
type="text"
|
|
43
|
+
value={draft}
|
|
44
|
+
placeholder={placeholder}
|
|
45
|
+
onChange={(e) => {
|
|
46
|
+
setDraft(e.target.value);
|
|
47
|
+
debouncedRef.current?.(e.target.value);
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</FilterFieldWrapper>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default per-node row used when no `renderResult` is supplied for a source.
|
|
3
|
+
*
|
|
4
|
+
* Layout:
|
|
5
|
+
* - Title: first entry in `displayFields` (preferring displayValue).
|
|
6
|
+
* - Subtitle: remaining display fields joined with " · ".
|
|
7
|
+
* - When `routePattern` is configured on the source, the row is wrapped in
|
|
8
|
+
* a react-router `<Link>` whose `to` is built by substituting `:fieldName`
|
|
9
|
+
* tokens with the corresponding field values (special token `:id`
|
|
10
|
+
* resolves to the configured `idField`, default "Id").
|
|
11
|
+
*
|
|
12
|
+
* Applications can still pass `renderResult[sourceKey]` to override.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ReactNode } from "react";
|
|
16
|
+
import { Link } from "react-router";
|
|
17
|
+
import { fieldValue } from "../../utils/fieldUtils";
|
|
18
|
+
import type { DisplayField, SObjectSourceConfig } from "../../types";
|
|
19
|
+
|
|
20
|
+
const ROUTE_TOKEN = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
21
|
+
|
|
22
|
+
function fieldName(f: DisplayField): string {
|
|
23
|
+
return typeof f === "string" ? f : f.name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolves a single dot-path against a node, returning a string or null. */
|
|
27
|
+
function readPath(node: unknown, path: string): string | null {
|
|
28
|
+
if (!node || typeof node !== "object") return null;
|
|
29
|
+
const parts = path.split(".");
|
|
30
|
+
let cursor: unknown = node;
|
|
31
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
32
|
+
if (!cursor || typeof cursor !== "object") return null;
|
|
33
|
+
cursor = (cursor as Record<string, unknown>)[parts[i]];
|
|
34
|
+
}
|
|
35
|
+
const leaf = (cursor as Record<string, unknown> | null)?.[parts[parts.length - 1]];
|
|
36
|
+
if (leaf == null) return null;
|
|
37
|
+
if (typeof leaf === "object") {
|
|
38
|
+
return fieldValue(leaf as { value?: unknown; displayValue?: string | null });
|
|
39
|
+
}
|
|
40
|
+
return String(leaf);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Reads the user-facing string for a top-level display field. */
|
|
44
|
+
function readDisplayField(node: unknown, field: DisplayField): string | null {
|
|
45
|
+
if (typeof field === "string") return readPath(node, field);
|
|
46
|
+
if ("raw" in field) return readPath(node, field.name);
|
|
47
|
+
// Relationship: prefer the first scalar subfield's display value.
|
|
48
|
+
const sub = field.subfields[0];
|
|
49
|
+
if (!sub) return null;
|
|
50
|
+
const subName = fieldName(sub);
|
|
51
|
+
return readPath(node, `${field.name}.${subName}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Substitutes `:fieldName` tokens in routePattern with node values. */
|
|
55
|
+
function resolveRoute(pattern: string, node: unknown, idField: string): string | null {
|
|
56
|
+
let failed = false;
|
|
57
|
+
const out = pattern.replace(ROUTE_TOKEN, (_, token) => {
|
|
58
|
+
const path = token === "id" ? idField : token;
|
|
59
|
+
const value = readPath(node, path);
|
|
60
|
+
if (value == null) {
|
|
61
|
+
failed = true;
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
return encodeURIComponent(value);
|
|
65
|
+
});
|
|
66
|
+
return failed ? null : out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface DefaultResultRowProps {
|
|
70
|
+
node: unknown;
|
|
71
|
+
source: SObjectSourceConfig;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function DefaultResultRow({ node, source }: DefaultResultRowProps) {
|
|
75
|
+
const idField = source.idField ?? "Id";
|
|
76
|
+
const fields = source.displayFields;
|
|
77
|
+
const titleField = fields[0];
|
|
78
|
+
const title = titleField ? readDisplayField(node, titleField) : null;
|
|
79
|
+
|
|
80
|
+
const subtitleParts = fields
|
|
81
|
+
.slice(1)
|
|
82
|
+
.map((f) => readDisplayField(node, f))
|
|
83
|
+
.filter((s): s is string => s != null && s !== "");
|
|
84
|
+
|
|
85
|
+
const content: ReactNode = (
|
|
86
|
+
<>
|
|
87
|
+
<span className="font-medium">{title ?? "—"}</span>
|
|
88
|
+
{subtitleParts.length > 0 && (
|
|
89
|
+
<p className="text-sm text-muted-foreground">{subtitleParts.join(" · ")}</p>
|
|
90
|
+
)}
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (source.routePattern) {
|
|
95
|
+
const href = resolveRoute(source.routePattern, node, idField);
|
|
96
|
+
if (href) {
|
|
97
|
+
return (
|
|
98
|
+
<Link
|
|
99
|
+
to={href}
|
|
100
|
+
className="block hover:bg-accent rounded-md px-3 -mx-3 py-1 transition-colors"
|
|
101
|
+
>
|
|
102
|
+
{content}
|
|
103
|
+
</Link>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return <div className="px-3 -mx-3 py-1">{content}</div>;
|
|
109
|
+
}
|
package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sources": [
|
|
3
|
+
{
|
|
4
|
+
"kind": "sobject",
|
|
5
|
+
"key": "accounts",
|
|
6
|
+
"objectName": "Account",
|
|
7
|
+
"label": "Accounts",
|
|
8
|
+
"routePattern": "/accounts/:id",
|
|
9
|
+
"searchableFields": ["Name", "Phone", "Industry"],
|
|
10
|
+
"displayFields": [
|
|
11
|
+
"Name",
|
|
12
|
+
"Industry",
|
|
13
|
+
"Type",
|
|
14
|
+
"Phone",
|
|
15
|
+
"AnnualRevenue",
|
|
16
|
+
{ "name": "Owner", "subfields": ["Name"] }
|
|
17
|
+
],
|
|
18
|
+
"filterFields": [
|
|
19
|
+
{ "field": "Industry", "label": "Industry", "type": "picklist" },
|
|
20
|
+
{ "field": "Type", "label": "Type", "type": "picklist" },
|
|
21
|
+
{ "field": "AnnualRevenue", "label": "Annual Revenue", "type": "numeric" }
|
|
22
|
+
],
|
|
23
|
+
"sortFields": [
|
|
24
|
+
{ "field": "Name", "label": "Name" },
|
|
25
|
+
{ "field": "Industry", "label": "Industry" },
|
|
26
|
+
{ "field": "AnnualRevenue", "label": "Annual Revenue" },
|
|
27
|
+
{ "field": "CreatedDate", "label": "Created Date" }
|
|
28
|
+
],
|
|
29
|
+
"pageSize": 10,
|
|
30
|
+
"validPageSizes": [5, 10, 20]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"kind": "sobject",
|
|
34
|
+
"key": "contacts",
|
|
35
|
+
"objectName": "Contact",
|
|
36
|
+
"label": "Contacts",
|
|
37
|
+
"routePattern": "/contacts/:id",
|
|
38
|
+
"searchableFields": ["Name", "Email", "Phone"],
|
|
39
|
+
"displayFields": [
|
|
40
|
+
"Name",
|
|
41
|
+
"Title",
|
|
42
|
+
"Email",
|
|
43
|
+
"Phone",
|
|
44
|
+
{ "name": "Account", "subfields": ["Name"] }
|
|
45
|
+
],
|
|
46
|
+
"sortFields": [
|
|
47
|
+
{ "field": "Name", "label": "Name" },
|
|
48
|
+
{ "field": "CreatedDate", "label": "Created Date" }
|
|
49
|
+
],
|
|
50
|
+
"pageSize": 10,
|
|
51
|
+
"validPageSizes": [5, 10, 20]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"kind": "sobject",
|
|
55
|
+
"key": "opportunities",
|
|
56
|
+
"objectName": "Opportunity",
|
|
57
|
+
"label": "Opportunities",
|
|
58
|
+
"routePattern": "/opportunities/:id",
|
|
59
|
+
"searchableFields": ["Name"],
|
|
60
|
+
"displayFields": [
|
|
61
|
+
"Name",
|
|
62
|
+
"StageName",
|
|
63
|
+
"Amount",
|
|
64
|
+
"CloseDate",
|
|
65
|
+
{ "name": "Account", "subfields": ["Name"] }
|
|
66
|
+
],
|
|
67
|
+
"filterFields": [
|
|
68
|
+
{ "field": "StageName", "label": "Stage", "type": "picklist" },
|
|
69
|
+
{ "field": "Amount", "label": "Amount", "type": "numeric" },
|
|
70
|
+
{ "field": "CloseDate", "label": "Close Date", "type": "daterange" }
|
|
71
|
+
],
|
|
72
|
+
"sortFields": [
|
|
73
|
+
{ "field": "CloseDate", "label": "Close Date" },
|
|
74
|
+
{ "field": "Amount", "label": "Amount" },
|
|
75
|
+
{ "field": "Name", "label": "Name" }
|
|
76
|
+
],
|
|
77
|
+
"defaultSort": { "field": "CloseDate", "direction": "DESC" },
|
|
78
|
+
"pageSize": 10,
|
|
79
|
+
"validPageSizes": [5, 10, 20]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseAsyncDataResult<T> {
|
|
4
|
+
data: T | null;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs an async fetcher on mount and whenever `deps` change. Stale responses
|
|
11
|
+
* (deps changed during a fetch, or component unmounted) are dropped via a
|
|
12
|
+
* cancellation flag.
|
|
13
|
+
*/
|
|
14
|
+
export function useAsyncData<T>(
|
|
15
|
+
fetcher: () => Promise<T>,
|
|
16
|
+
deps: React.DependencyList,
|
|
17
|
+
): UseAsyncDataResult<T> {
|
|
18
|
+
const [data, setData] = useState<T | null>(null);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const [generation, setGeneration] = useState(0);
|
|
22
|
+
|
|
23
|
+
const fetcherRef = useRef(fetcher);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
fetcherRef.current = fetcher;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const [prevDeps, setPrevDeps] = useState(deps);
|
|
29
|
+
if (deps.length !== prevDeps.length || deps.some((d, i) => d !== prevDeps[i])) {
|
|
30
|
+
setPrevDeps(deps);
|
|
31
|
+
setGeneration((g) => g + 1);
|
|
32
|
+
if (!loading) setLoading(true);
|
|
33
|
+
if (error !== null) setError(null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
fetcherRef
|
|
39
|
+
.current()
|
|
40
|
+
.then((result) => {
|
|
41
|
+
if (!cancelled) setData(result);
|
|
42
|
+
})
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
console.error(err);
|
|
45
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "An error occurred");
|
|
46
|
+
})
|
|
47
|
+
.finally(() => {
|
|
48
|
+
if (!cancelled) setLoading(false);
|
|
49
|
+
});
|
|
50
|
+
return () => {
|
|
51
|
+
cancelled = true;
|
|
52
|
+
};
|
|
53
|
+
}, [generation]);
|
|
54
|
+
|
|
55
|
+
return { data, loading, error };
|
|
56
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caches and serves distinct picklist values fetched via the GraphQL
|
|
3
|
+
* aggregate API. The cache key is `${objectName}.${fieldName}`; entries
|
|
4
|
+
* persist for the lifetime of the page so repeated mounts of the same
|
|
5
|
+
* picklist filter share the same response.
|
|
6
|
+
*
|
|
7
|
+
* Pass `null` to short-circuit the fetch (e.g. when inline options are
|
|
8
|
+
* available on the filter config).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
|
+
import { fetchDistinctValues, type PicklistOption } from "../api/distinctValuesService";
|
|
13
|
+
|
|
14
|
+
const cache = new Map<string, PicklistOption[]>();
|
|
15
|
+
const inflight = new Map<string, Promise<PicklistOption[]>>();
|
|
16
|
+
|
|
17
|
+
interface DistinctValuesQuery {
|
|
18
|
+
objectName: string;
|
|
19
|
+
fieldName: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useDistinctValues(query: DistinctValuesQuery | null): PicklistOption[] | null {
|
|
23
|
+
const cacheKey = query ? `${query.objectName}.${query.fieldName}` : null;
|
|
24
|
+
const [options, setOptions] = useState<PicklistOption[] | null>(() =>
|
|
25
|
+
cacheKey ? (cache.get(cacheKey) ?? null) : null,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!query || !cacheKey) return;
|
|
30
|
+
const cached = cache.get(cacheKey);
|
|
31
|
+
if (cached) {
|
|
32
|
+
setOptions(cached);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
let cancelled = false;
|
|
36
|
+
const existing = inflight.get(cacheKey);
|
|
37
|
+
const promise =
|
|
38
|
+
existing ??
|
|
39
|
+
fetchDistinctValues(query.objectName, query.fieldName).then((result) => {
|
|
40
|
+
cache.set(cacheKey, result);
|
|
41
|
+
inflight.delete(cacheKey);
|
|
42
|
+
return result;
|
|
43
|
+
});
|
|
44
|
+
if (!existing) inflight.set(cacheKey, promise);
|
|
45
|
+
promise
|
|
46
|
+
.then((result) => {
|
|
47
|
+
if (!cancelled) setOptions(result);
|
|
48
|
+
})
|
|
49
|
+
.catch((err: unknown) => {
|
|
50
|
+
inflight.delete(cacheKey);
|
|
51
|
+
console.error(`Failed to fetch distinct values for ${cacheKey}:`, err);
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
cancelled = true;
|
|
55
|
+
};
|
|
56
|
+
}, [cacheKey, query]);
|
|
57
|
+
|
|
58
|
+
return options;
|
|
59
|
+
}
|