@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.
Files changed (117) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
  3. package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
  4. package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
  116. package/dist/package.json +1 -1
  117. package/package.json +5 -1
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared allowlist for link protocols (e.g. for FormattedUrl, and future mailto/tel if rendered as links).
3
+ * Centralizes protocol checks so new link types can be added in one place.
4
+ */
5
+ export const ALLOWED_LINK_PROTOCOLS = ["http:", "https:"] as const;
6
+
7
+ export function isAllowedLinkUrl(value: string): boolean {
8
+ try {
9
+ const u = new URL(value);
10
+ return (ALLOWED_LINK_PROTOCOLS as readonly string[]).includes(u.protocol);
11
+ } catch {
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Pagination Utilities
3
+ *
4
+ * Utility functions for pagination-related operations including page size validation.
5
+ */
6
+
7
+ /**
8
+ * Default page size options for pagination
9
+ */
10
+ export const PAGE_SIZE_OPTIONS = [
11
+ { value: "10", label: "10" },
12
+ { value: "20", label: "20" },
13
+ { value: "50", label: "50" },
14
+ ] as const;
15
+
16
+ /**
17
+ * Valid page size values extracted from PAGE_SIZE_OPTIONS
18
+ */
19
+ export const VALID_PAGE_SIZES = PAGE_SIZE_OPTIONS.map((opt) => parseInt(opt.value, 10));
20
+
21
+ /**
22
+ * Validates that a page size is one of the allowed options
23
+ * @param size - The page size to validate
24
+ * @returns true if valid, false otherwise
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * if (isValidPageSize(userInput)) {
29
+ * setPageSize(userInput);
30
+ * }
31
+ * ```
32
+ */
33
+ export function isValidPageSize(size: number): boolean {
34
+ return VALID_PAGE_SIZES.includes(size);
35
+ }
36
+
37
+ /**
38
+ * Gets a valid page size, defaulting to the first option if invalid
39
+ * @param size - The page size to validate
40
+ * @returns A valid page size
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const safePageSize = getValidPageSize(userInput); // Returns valid size or default
45
+ * ```
46
+ */
47
+ export function getValidPageSize(size: number): number {
48
+ return isValidPageSize(size) ? size : VALID_PAGE_SIZES[0];
49
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Record utilities: layout-derived fields for GraphQL fetch, safe keys, ID validation.
3
+ *
4
+ * calculateFieldsToFetch: from layout + object metadata → field names and relation map;
5
+ * used by objectDetailService and list to build columns. findIdFieldForRelationship ensures
6
+ * Id + relationship name are both requested for reference display.
7
+ *
8
+ * @module utils/recordUtils
9
+ */
10
+
11
+ import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
12
+ import type {
13
+ LayoutResponse,
14
+ LayoutRow,
15
+ LayoutSection,
16
+ LayoutItem,
17
+ LayoutComponent,
18
+ } from "../types/recordDetail/recordDetail";
19
+
20
+ /**
21
+ * Find the Id field (reference foreign key) whose relationshipName matches the given name,
22
+ * so we can request both Id and relationship in the record query for display.
23
+ */
24
+ function findIdFieldForRelationship(
25
+ metadata: ObjectInfoResult,
26
+ relationshipName: string,
27
+ ): string | null {
28
+ if (!metadata.fields || !relationshipName) return null;
29
+ for (const [apiName, field] of Object.entries(metadata.fields)) {
30
+ const isReference = field.dataType != null && field.dataType.toLowerCase() === "reference";
31
+ if (field.relationshipName === relationshipName && (isReference || apiName.endsWith("Id"))) {
32
+ return apiName;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+
38
+ const getFetchableFieldsFromLayoutItem = function (
39
+ metadata: ObjectInfoResult,
40
+ layoutItem: LayoutItem,
41
+ relationFieldMap: Record<string, string>,
42
+ ) {
43
+ const fields: Record<string, string> = {};
44
+ layoutItem.layoutComponents.forEach((comp: LayoutComponent) => {
45
+ // check if this is a field to add
46
+ if (!comp.apiName || comp.componentType !== "Field") {
47
+ return;
48
+ }
49
+
50
+ // add field: fieldType
51
+ const fieldMetadata = metadata.fields[comp.apiName];
52
+ fields[comp.apiName] = fieldMetadata ? fieldMetadata.dataType : "";
53
+
54
+ // add relatedField if one exists (Id field -> add relationship name so we request Owner.Name)
55
+ if (comp.apiName in metadata.fields) {
56
+ const relationshipName = fieldMetadata?.relationshipName;
57
+ if (relationshipName) {
58
+ fields[relationshipName] = fieldMetadata.dataType;
59
+
60
+ relationFieldMap[comp.apiName] = relationshipName;
61
+ }
62
+ } else {
63
+ // layout component is relationship name (e.g. Owner); ensure we also request the Id
64
+ // so buildSelectionTree sees both OwnerId and Owner and requests Owner { Name { value } }
65
+ const idField = findIdFieldForRelationship(metadata, comp.apiName);
66
+ if (idField) {
67
+ const idMeta = metadata.fields[idField];
68
+ fields[idField] = idMeta ? idMeta.dataType : "";
69
+ relationFieldMap[idField] = comp.apiName;
70
+ }
71
+ }
72
+ });
73
+ return fields;
74
+ };
75
+
76
+ const getFetchableFieldsFromLayoutRow = function (
77
+ metadata: ObjectInfoResult,
78
+ layoutRow: LayoutRow,
79
+ relationFieldMap: Record<string, string>,
80
+ ) {
81
+ let fieldsFromRow: Record<string, string> = {};
82
+ layoutRow.layoutItems.forEach((item: LayoutItem) => {
83
+ Object.assign(
84
+ fieldsFromRow,
85
+ getFetchableFieldsFromLayoutItem(metadata, item, relationFieldMap),
86
+ );
87
+ });
88
+ return fieldsFromRow;
89
+ };
90
+
91
+ const getFetchableFieldsFromSection = function (
92
+ metadata: ObjectInfoResult,
93
+ section: LayoutSection,
94
+ relationFieldMap: Record<string, string>,
95
+ ) {
96
+ let fieldsFromSection: Record<string, string> = {};
97
+ section.layoutRows.forEach((row: LayoutRow) => {
98
+ Object.assign(
99
+ fieldsFromSection,
100
+ getFetchableFieldsFromLayoutRow(metadata, row, relationFieldMap),
101
+ );
102
+ });
103
+ return fieldsFromSection;
104
+ };
105
+
106
+ const getFetchableFieldsFromLayout = function (
107
+ metadata: ObjectInfoResult,
108
+ layout: LayoutResponse,
109
+ relationFieldMap: Record<string, string>,
110
+ ) {
111
+ let fieldsFromLayout: Record<string, string> = {};
112
+ layout.sections.forEach((section) => {
113
+ Object.assign(
114
+ fieldsFromLayout,
115
+ getFetchableFieldsFromSection(metadata, section, relationFieldMap),
116
+ );
117
+ });
118
+ return fieldsFromLayout;
119
+ };
120
+
121
+ /**
122
+ * Returns field API names to request for records from layout + object metadata.
123
+ * Includes both Id and relationship name for reference fields so GraphQL can fetch display value.
124
+ *
125
+ * @param metadata - Object info (fields with dataType, relationshipName).
126
+ * @param layout - Layout response (sections, layoutItems, layoutComponents).
127
+ * @param shouldPrefixedWithEntityName - If true, prefix names with object (e.g. Account.Name).
128
+ * @returns [fieldNames, fieldTypes, relationFieldMap] for buildSelectionTree / optionalFields.
129
+ */
130
+ export const calculateFieldsToFetch = function (
131
+ metadata: ObjectInfoResult,
132
+ layout: LayoutResponse,
133
+ shouldPrefixedWithEntityName: boolean,
134
+ ): [string[], string[], Record<string, string>] {
135
+ const relationFieldMap: Record<string, string> = {};
136
+ // populating fields to query for layout
137
+ const fields = getFetchableFieldsFromLayout(metadata, layout, relationFieldMap);
138
+ let fieldsToFetch = Object.keys(fields);
139
+ if (shouldPrefixedWithEntityName) {
140
+ fieldsToFetch = fieldsToFetch.map((field) => `${metadata.apiName}.${field}`);
141
+ }
142
+ // populate field types for o11y logging
143
+ const fieldTypes = Object.values(fields).filter((fieldType) => fieldType !== "");
144
+ return [fieldsToFetch, fieldTypes, relationFieldMap];
145
+ };
146
+ /** Type guard: true if id is a non-empty string matching 15- or 18-char Salesforce ID format. */
147
+ export function isValidSalesforceId(id: string | null | undefined): id is string {
148
+ if (!id || typeof id !== "string") return false;
149
+ return /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/.test(id);
150
+ }
151
+
152
+ /** Safe React key from record id or fallback to prefix-index. */
153
+ export function getSafeKey(
154
+ recordId: string | null | undefined,
155
+ index: number,
156
+ prefix: string = "result",
157
+ ): string {
158
+ return isValidSalesforceId(recordId) ? recordId : `${prefix}-${index}`;
159
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Sanitization Utilities
3
+ *
4
+ * Utility functions for sanitizing user input to prevent injection attacks.
5
+ * These utilities provide basic sanitization for filter values.
6
+ */
7
+
8
+ /**
9
+ * Sanitizes a string value by removing potentially dangerous characters
10
+ * and trimming whitespace.
11
+ *
12
+ * This is a basic sanitization - for production, consider using a library like DOMPurify for more
13
+ * comprehensive sanitization.
14
+ * Also, note this is NOT an end-to-end security control.
15
+ * Client-side sanitization can be bypassed by any attacker using `curl` or Postman.
16
+ * To prevent injection attacks (SOSL Injection, XSS):
17
+ * 1. The BACKEND (Salesforce API) handles SOSL injection if parameters are passed correctly.
18
+ * 2. React handles XSS automatically when rendering variables in JSX (e.g., <div>{value}</div>).
19
+ * Do not rely on this function for end-to-end security enforcement.
20
+ *
21
+ * @param value - The string value to sanitize
22
+ * @returns Sanitized string value
23
+ *
24
+ * @remarks
25
+ * - Removes control characters (except newlines, tabs, carriage returns)
26
+ * - Trims leading/trailing whitespace
27
+ * - Limits length to prevent DoS attacks (default: 1000 characters)
28
+ * - Preserves alphanumeric, spaces, and common punctuation
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * const sanitized = sanitizeFilterValue(userInput);
33
+ * ```
34
+ */
35
+ export function sanitizeFilterValue(value: string, maxLength: number = 1000): string {
36
+ if (typeof value !== "string") {
37
+ return "";
38
+ }
39
+
40
+ let sanitized = value.trim();
41
+
42
+ if (sanitized.length > maxLength) {
43
+ sanitized = sanitized.substring(0, maxLength);
44
+ }
45
+
46
+ sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
47
+
48
+ return sanitized;
49
+ }
@@ -0,0 +1,29 @@
1
+ import { useState, useRef, useEffect, type Dispatch, type SetStateAction } from "react";
2
+
3
+ /**
4
+ * Accumulates list pages when the GraphQL hook returns one page at a time.
5
+ * When loading goes from true to false: if afterCursor is null, replaces the list;
6
+ * otherwise appends (load more). Caller must reset accumulated when search/filters/sort change.
7
+ */
8
+ export function useAccumulatedListPages<T>(
9
+ edges: Array<{ node?: unknown }>,
10
+ loading: boolean,
11
+ afterCursor: string | null,
12
+ mapNode: (node: unknown) => T,
13
+ ): [T[], Dispatch<SetStateAction<T[]>>] {
14
+ const [accumulated, setAccumulated] = useState<T[]>([]);
15
+ const prevLoadingRef = useRef(loading);
16
+
17
+ useEffect(() => {
18
+ const isFirstPage = afterCursor === null;
19
+ const justFinishedLoading = prevLoadingRef.current && !loading;
20
+ if (justFinishedLoading) {
21
+ const list = edges.map((e) => mapNode(e.node));
22
+ if (isFirstPage) setAccumulated(list);
23
+ else setAccumulated((prev) => [...prev, ...list]);
24
+ }
25
+ prevLoadingRef.current = loading;
26
+ }, [loading, edges, afterCursor, mapNode]);
27
+
28
+ return [accumulated, setAccumulated];
29
+ }
@@ -0,0 +1,167 @@
1
+ import { useEffect, useState, useRef, useMemo, useCallback } from "react";
2
+ import { useSearchParams } from "react-router";
3
+ import {
4
+ useObjectListMetadata,
5
+ useRecordListGraphQL,
6
+ type FilterCriteria,
7
+ } from "@salesforce/webapp-template-feature-react-global-search-experimental";
8
+ import { PAGE_SIZE_LIST } from "../lib/constants.js";
9
+ import {
10
+ buildFilterCriteriaFromFormValues,
11
+ getDefaultFilterFormValues,
12
+ getApplicableFilters,
13
+ } from "../lib/filterUtils.js";
14
+ import { useAccumulatedListPages } from "./useAccumulatedListPages.js";
15
+ import type { ListPageConfig } from "../lib/listPageConfig.js";
16
+
17
+ /** Picklist option shape from useObjectListMetadata (label/value). */
18
+ export interface PicklistOption {
19
+ label?: string;
20
+ value: string;
21
+ }
22
+
23
+ export interface UseListPageResult<T> {
24
+ filters: ReturnType<typeof getApplicableFilters>;
25
+ picklistValues: Record<string, PicklistOption[]>;
26
+ formValues: Record<string, string>;
27
+ onFormValueChange: (key: string, value: string) => void;
28
+ onApplyFilters: () => void;
29
+ onResetFilters: () => void;
30
+ filterError: string | null;
31
+ loading: boolean;
32
+ error: string | null;
33
+ items: T[];
34
+ canLoadMore: boolean;
35
+ onLoadMore: () => void;
36
+ loadMoreLoading: boolean;
37
+ sortBy?: string;
38
+ onSortChange?: (sortBy: string) => void;
39
+ }
40
+
41
+ /**
42
+ * Shared hook for list pages with API-driven filters and GraphQL data.
43
+ * Uses useObjectListMetadata and useRecordListGraphQL; search comes from URL ?q=.
44
+ * Pass the returned props to ListPageWithFilters and render your table/list as children.
45
+ */
46
+ export function useListPage<T>(config: ListPageConfig<T>): UseListPageResult<T> {
47
+ const [searchParams] = useSearchParams();
48
+ const searchQuery = searchParams.get("q") ?? "";
49
+
50
+ const [afterCursor, setAfterCursor] = useState<string | null>(null);
51
+ const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
52
+ const [filterFormValues, setFilterFormValues] = useState<Record<string, string>>({});
53
+ const [filterError, setFilterError] = useState<string | null>(null);
54
+ const hasInitializedFiltersRef = useRef(false);
55
+ const [sortBy, setSortBy] = useState(config.defaultSort);
56
+
57
+ const listMeta = useObjectListMetadata(config.objectApiName);
58
+ const columns = useMemo(() => config.getColumns(listMeta.columns), [listMeta.columns, config]);
59
+ const filters = useMemo(
60
+ () => getApplicableFilters(listMeta.filters ?? [], config.filterExcludedFieldPaths),
61
+ [listMeta.filters, config.filterExcludedFieldPaths],
62
+ );
63
+ const picklistValues = listMeta.picklistValues ?? {};
64
+
65
+ const effectiveSort = config.sortable ? sortBy : config.defaultSort;
66
+
67
+ const {
68
+ edges,
69
+ pageInfo,
70
+ loading: resultsLoading,
71
+ error: resultsError,
72
+ } = useRecordListGraphQL({
73
+ objectApiName: config.objectApiName,
74
+ columns,
75
+ columnsLoading: listMeta.loading,
76
+ columnsError: listMeta.error,
77
+ first: PAGE_SIZE_LIST,
78
+ after: afterCursor,
79
+ searchQuery: searchQuery.trim() || undefined,
80
+ sortBy: effectiveSort,
81
+ filters: appliedFilters,
82
+ });
83
+
84
+ const mapNode = useCallback((node: unknown) => config.nodeToItem(node), [config]);
85
+
86
+ const [accumulated, setAccumulated] = useAccumulatedListPages(
87
+ edges,
88
+ resultsLoading,
89
+ afterCursor,
90
+ mapNode,
91
+ );
92
+
93
+ useEffect(() => {
94
+ if (filters.length === 0) return;
95
+ if (!hasInitializedFiltersRef.current) {
96
+ hasInitializedFiltersRef.current = true;
97
+ setFilterFormValues(getDefaultFilterFormValues(filters));
98
+ }
99
+ }, [filters]);
100
+
101
+ useEffect(() => {
102
+ setAfterCursor(null);
103
+ setAccumulated([]);
104
+ }, [searchQuery, appliedFilters, effectiveSort, setAccumulated]);
105
+
106
+ const loading = listMeta.loading || resultsLoading;
107
+ const error = listMeta.error ?? resultsError ?? null;
108
+ const hasNextPage = Boolean(pageInfo?.hasNextPage);
109
+ const endCursor = pageInfo?.endCursor ?? null;
110
+
111
+ const onLoadMore = useCallback(() => {
112
+ if (endCursor && !searchQuery.trim()) setAfterCursor(endCursor);
113
+ }, [endCursor, searchQuery]);
114
+
115
+ const onApplyFilters = useCallback(() => {
116
+ setFilterError(null);
117
+ const result = buildFilterCriteriaFromFormValues(
118
+ config.objectApiName,
119
+ filters,
120
+ filterFormValues,
121
+ );
122
+ if (result.rangeError) {
123
+ setFilterError(result.rangeError);
124
+ return;
125
+ }
126
+ setAppliedFilters(result.criteria);
127
+ setAfterCursor(null);
128
+ }, [config.objectApiName, filters, filterFormValues]);
129
+
130
+ const onResetFilters = useCallback(() => {
131
+ setFilterFormValues(getDefaultFilterFormValues(filters));
132
+ setAppliedFilters([]);
133
+ setAfterCursor(null);
134
+ setFilterError(null);
135
+ }, [filters]);
136
+
137
+ const onFormValueChange = useCallback((key: string, value: string) => {
138
+ setFilterFormValues((prev) => ({ ...prev, [key]: value }));
139
+ setFilterError(null);
140
+ }, []);
141
+
142
+ const onSortChange = useCallback((newSortBy: string) => {
143
+ setSortBy(newSortBy);
144
+ setAfterCursor(null);
145
+ }, []);
146
+
147
+ const result: UseListPageResult<T> = {
148
+ filters,
149
+ picklistValues: picklistValues as Record<string, PicklistOption[]>,
150
+ formValues: filterFormValues,
151
+ onFormValueChange,
152
+ onApplyFilters,
153
+ onResetFilters,
154
+ filterError,
155
+ loading,
156
+ error,
157
+ items: accumulated,
158
+ canLoadMore: hasNextPage && !searchQuery.trim(),
159
+ onLoadMore,
160
+ loadMoreLoading: resultsLoading,
161
+ };
162
+ if (config.sortable) {
163
+ result.sortBy = sortBy;
164
+ result.onSortChange = onSortChange;
165
+ }
166
+ return result;
167
+ }
@@ -1,6 +1,10 @@
1
1
  /**
2
- * feature-react-agentforce-conversation-client – ACC Conversation Client
2
+ * Package entry for @salesforce/webapp-template-feature-react-global-search-experimental.
3
+ * Exports only API, hooks, utils, types, and constants so that consuming apps do not
4
+ * pull in the feature's UI (routes, __inherit__ components) which depend on @radix-ui.
3
5
  */
4
-
5
- export { AgentforceConversationClient } from "./components/AgentforceConversationClient";
6
- export type { AgentforceConversationClientProps, ResolvedEmbedOptions } from "./types/conversation";
6
+ export * from "./features/global-search/api";
7
+ export * from "./features/global-search/hooks";
8
+ export * from "./features/global-search/utils";
9
+ export * from "./features/global-search/types";
10
+ export * from "./constants";
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Adapter: maps Application__c GraphQL node to app Application type.
3
+ */
4
+ import type { Application } from "./types.js";
5
+
6
+ interface ApplicationNode {
7
+ Id?: string;
8
+ Name?: { value?: string };
9
+ Status__c?: { value?: string };
10
+ User__r?: { Name?: { value?: string } };
11
+ Property__r?: { Address__c?: { value?: string }; Name?: { value?: string } };
12
+ CreatedDate?: { value?: string };
13
+ Employment__c?: { value?: string };
14
+ References__c?: { value?: string };
15
+ Start_Date__c?: { value?: string };
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ export function nodeToApplication(node: Record<string, unknown> | undefined): Application {
20
+ const n = (node ?? {}) as ApplicationNode;
21
+ const created = n.CreatedDate?.value;
22
+ return {
23
+ id: n.Id ?? "",
24
+ applicantName: n.User__r?.Name?.value ?? "Unknown",
25
+ propertyAddress: n.Property__r?.Address__c?.value ?? "",
26
+ submittedDate: created ? new Date(created).toLocaleDateString() : "",
27
+ status: n.Status__c?.value ?? "",
28
+ employment: n.Employment__c?.value,
29
+ references: n.References__c?.value,
30
+ startDate: n.Start_Date__c?.value,
31
+ propertyName: n.Property__r?.Name?.value,
32
+ };
33
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Column config for Application__c list.
3
+ */
4
+ import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
5
+
6
+ export const APPLICATION_EXTRA_COLUMNS: Column[] = [
7
+ { fieldApiName: "Name", label: "Name", searchable: true, sortable: true },
8
+ { fieldApiName: "Status__c", label: "Status", searchable: true, sortable: true },
9
+ { fieldApiName: "User__r.Name", label: "Applicant", searchable: true, sortable: false },
10
+ {
11
+ fieldApiName: "Property__r.Address__c",
12
+ label: "Property Address",
13
+ searchable: true,
14
+ sortable: false,
15
+ },
16
+ { fieldApiName: "Property__r.Name", label: "Property Name", searchable: true, sortable: false },
17
+ { fieldApiName: "CreatedDate", label: "Submitted", searchable: false, sortable: true },
18
+ { fieldApiName: "Employment__c", label: "Employment", searchable: true, sortable: false },
19
+ { fieldApiName: "References__c", label: "References", searchable: true, sortable: false },
20
+ { fieldApiName: "Start_Date__c", label: "Start Date", searchable: false, sortable: true },
21
+ ];
22
+
23
+ export function getApplicationListColumns(columns: Column[]): Column[] {
24
+ if (columns.length === 0) return APPLICATION_EXTRA_COLUMNS;
25
+ const existing = new Set(columns.map((c) => c.fieldApiName));
26
+ const toAdd = APPLICATION_EXTRA_COLUMNS.filter((c) => !existing.has(c.fieldApiName));
27
+ return toAdd.length === 0 ? columns : [...columns, ...toAdd];
28
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Application-wide constants.
3
+ */
4
+
5
+ export const PAGE_SIZE_LIST = 12;
6
+ export const DASHBOARD_MAINTENANCE_LIMIT = 5;
7
+
8
+ export const PROPERTY_FILTER_EXCLUDED_FIELD_PATHS = new Set([
9
+ "CreatedDate",
10
+ "Hero_Image__c",
11
+ "Year_Built__c",
12
+ "Available_Date__c",
13
+ ]);
14
+
15
+ export const MAINTENANCE_WORKER_FILTER_EXCLUDED_FIELD_PATHS = new Set<string>([]);
16
+ export const MAINTENANCE_FILTER_EXCLUDED_FIELD_PATHS = new Set(["Scheduled__c"]);
17
+ export const APPLICATION_FILTER_EXCLUDED_FIELD_PATHS = new Set<string>([]);
18
+
19
+ export const MAINTENANCE_WORKER_OBJECT_API_NAME = "Maintenance_Worker__c" as const;
20
+
21
+ export const FALLBACK_LABEL_PROPERTIES_PLURAL = "Properties";
22
+ export const FALLBACK_LABEL_MAINTENANCE_PLURAL = "Maintenance Requests";
23
+ export const FALLBACK_LABEL_MAINTENANCE_WORKERS_PLURAL = "Maintenance Workers";
24
+ export const FALLBACK_LABEL_APPLICATIONS_PLURAL = "Applications";
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Maps Salesforce API field paths (from getObjectListFilters) to our app record properties.
3
+ * Used by applyFilterCriteria when filtering list data with FilterCriteria from the feature API.
4
+ */
5
+ import type { Property, MaintenanceRequest, Application } from "./types.js";
6
+
7
+ const PROPERTY_FIELD_MAP: Record<string, (p: Property) => string | number | undefined> = {
8
+ Name: (p) => p.name,
9
+ Address__c: (p) => p.address,
10
+ Type__c: (p) => p.type,
11
+ Status__c: (p) => p.status,
12
+ Description__c: (p) => p.description,
13
+ Monthly_Rent__c: (p) => p.monthlyRent,
14
+ Bedrooms__c: (p) => p.bedrooms,
15
+ Bathrooms__c: (p) => p.bathrooms,
16
+ Sq_Ft__c: (p) => p.sqFt,
17
+ Year_Built__c: (p) => p.yearBuilt,
18
+ };
19
+
20
+ const MAINTENANCE_REQUEST_FIELD_MAP: Record<
21
+ string,
22
+ (r: MaintenanceRequest) => string | number | undefined
23
+ > = {
24
+ Status__c: (r) => r.status,
25
+ Type__c: (r) => r.issueType,
26
+ Description__c: (r) => r.description,
27
+ Priority__c: (r) => r.priority,
28
+ propertyAddress: (r) => r.propertyAddress,
29
+ tenantName: (r) => r.tenantName,
30
+ "Property__r.Address__c": (r) => r.propertyAddress,
31
+ "Property__r.Name": (r) => r.tenantUnit ?? r.propertyAddress,
32
+ "User__r.Name": (r) => r.tenantName,
33
+ };
34
+
35
+ const APPLICATION_FIELD_MAP: Record<string, (a: Application) => string | number | undefined> = {
36
+ Status__c: (a) => a.status,
37
+ Start_Date__c: (a) => a.submittedDate || a.startDate,
38
+ applicantName: (a) => a.applicantName,
39
+ propertyAddress: (a) => a.propertyAddress,
40
+ Employment__c: (a) => a.employment,
41
+ References__c: (a) => a.references,
42
+ "Property__r.Address__c": (a) => a.propertyAddress,
43
+ "Property__r.Name": (a) => a.propertyName ?? a.propertyAddress,
44
+ };
45
+
46
+ export function getPropertyRecordValue(
47
+ record: Property,
48
+ fieldPath: string,
49
+ ): string | number | undefined {
50
+ const getter = PROPERTY_FIELD_MAP[fieldPath];
51
+ if (getter) return getter(record);
52
+ return (record as unknown as Record<string, unknown>)[fieldPath] as string | number | undefined;
53
+ }
54
+
55
+ export function getMaintenanceRequestRecordValue(
56
+ record: MaintenanceRequest,
57
+ fieldPath: string,
58
+ ): string | number | undefined {
59
+ const getter = MAINTENANCE_REQUEST_FIELD_MAP[fieldPath];
60
+ if (getter) return getter(record);
61
+ return (record as unknown as Record<string, unknown>)[fieldPath] as string | number | undefined;
62
+ }
63
+
64
+ export function getApplicationRecordValue(
65
+ record: Application,
66
+ fieldPath: string,
67
+ ): string | number | undefined {
68
+ const getter = APPLICATION_FIELD_MAP[fieldPath];
69
+ if (getter) return getter(record);
70
+ return (record as unknown as Record<string, unknown>)[fieldPath] as string | number | undefined;
71
+ }