@salesforce/webapp-template-app-react-sample-b2x-experimental 1.112.7 → 1.112.9

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 (138) hide show
  1. package/dist/CHANGELOG.md +19 -0
  2. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +6 -5
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +12058 -214
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +18 -15
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyListingGraphQL.ts → properties/propertyListingGraphQL.ts} +1 -1
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +4 -2
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{TopBar.tsx → layout/TopBar.tsx} +2 -2
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestList.tsx → maintenanceRequests/MaintenanceRequestList.tsx} +4 -4
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestListItem.tsx → maintenanceRequests/MaintenanceRequestListItem.tsx} +3 -3
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +87 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyListingCard.tsx → properties/PropertyListingCard.tsx} +1 -1
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/components/search/SearchPagination.tsx → components/properties/PropertyListingSearchPagination.tsx} +20 -28
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants/propertyListing.ts +4 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +303 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/FilterContext.tsx +73 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/PaginationControls.tsx +109 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SortControl.tsx +143 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/BooleanFilter.tsx +74 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateFilter.tsx +121 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +69 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +98 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +85 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SearchFilter.tsx +37 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SelectFilter.tsx +93 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/TextFilter.tsx +74 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useAsyncData.ts +54 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useCachedAsyncData.ts +184 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useObjectSearchParams.ts +247 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/debounce.ts +22 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +1 -1
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +1 -1
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +2 -2
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +2 -2
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +3 -3
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +2 -2
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +3 -3
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +2 -2
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +4 -2
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +2 -2
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +1 -1
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +12 -10
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +6 -18
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/propertyListingPaginationUtils.ts +18 -0
  59. package/dist/package-lock.json +2 -2
  60. package/dist/package.json +1 -1
  61. package/dist/scripts/graphql-search.sh +69 -17
  62. package/package.json +1 -1
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceDetailsModal.tsx +0 -128
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/constants.ts +0 -39
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/form.tsx +0 -209
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/filters.ts +0 -121
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/schema.d.ts +0 -200
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/debounce.ts +0 -90
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formUtils.ts +0 -142
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +0 -64
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/index.ts +0 -120
  126. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/About.tsx +0 -8
  127. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +0 -29
  128. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +0 -100
  129. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{applicationApi.ts → applications/applicationApi.ts} +0 -0
  130. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{maintenanceRequestApi.ts → maintenanceRequests/maintenanceRequestApi.ts} +0 -0
  131. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyDetailGraphQL.ts → properties/propertyDetailGraphQL.ts} +0 -0
  132. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{WeatherWidget.tsx → dashboard/WeatherWidget.tsx} +0 -0
  133. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{NavMenu.tsx → layout/VerticalNav.tsx} +0 -0
  134. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestIcon.tsx → maintenanceRequests/MaintenanceRequestIcon.tsx} +0 -0
  135. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{StatusBadge.tsx → maintenanceRequests/StatusBadge.tsx} +0 -0
  136. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyMap.tsx → properties/PropertyMap.tsx} +0 -0
  137. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertySearchFilters.tsx → properties/PropertySearchFilters.tsx} +0 -0
  138. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/types/search → types}/searchResults.ts +0 -0
@@ -0,0 +1,84 @@
1
+ import { createDataSDK } from "@salesforce/sdk-data";
2
+
3
+ export interface ObjectSearchOptions<TWhere, TOrderBy> {
4
+ where?: TWhere;
5
+ orderBy?: TOrderBy;
6
+ first?: number;
7
+ after?: string;
8
+ }
9
+
10
+ export type PicklistOption = { value: string; label: string };
11
+
12
+ /**
13
+ * Executes a GraphQL search query and extracts the result for the given object name
14
+ * from the standard `uiapi.query.<ObjectName>` response shape.
15
+ */
16
+ export async function searchObjects<TResult, TQuery, TVariables>(
17
+ query: string,
18
+ objectName: string,
19
+ options: ObjectSearchOptions<unknown, unknown> = {},
20
+ ): Promise<TResult> {
21
+ const { where, orderBy, first = 20, after } = options;
22
+
23
+ const data = await createDataSDK();
24
+ const response = await data.graphql?.<TQuery, TVariables>(query, {
25
+ first,
26
+ after,
27
+ where,
28
+ orderBy,
29
+ } as TVariables);
30
+
31
+ if (response?.errors?.length) {
32
+ throw new Error(response.errors.map((e) => e.message).join("; "));
33
+ }
34
+
35
+ const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
36
+ | Record<string, unknown>
37
+ | undefined;
38
+ const queryResult = (result?.query as Record<string, unknown> | undefined)?.[objectName] as
39
+ | TResult
40
+ | undefined;
41
+
42
+ if (!queryResult) {
43
+ throw new Error(`No ${objectName} data returned`);
44
+ }
45
+
46
+ return queryResult;
47
+ }
48
+
49
+ /**
50
+ * Executes a GraphQL aggregate/groupBy query and extracts picklist options
51
+ * from the standard `uiapi.aggregate.<ObjectName>` response shape.
52
+ */
53
+ export async function fetchDistinctValues<TQuery>(
54
+ query: string,
55
+ objectName: string,
56
+ fieldName: string,
57
+ ): Promise<PicklistOption[]> {
58
+ const data = await createDataSDK();
59
+ const response = await data.graphql?.<TQuery>(query);
60
+ const errors = response?.errors;
61
+
62
+ if (errors?.length) {
63
+ throw new Error(errors.map((e) => e.message).join("; "));
64
+ }
65
+
66
+ const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
67
+ | Record<string, unknown>
68
+ | undefined;
69
+ const aggregate = (result?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
70
+ | { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
71
+ | undefined;
72
+
73
+ const edges = aggregate?.edges ?? [];
74
+ return edges
75
+ .map((edge) => {
76
+ const field = edge?.node?.aggregate?.[fieldName] as
77
+ | { value?: string | null; displayValue?: string | null; label?: string | null }
78
+ | undefined;
79
+ const value = field?.value;
80
+ if (!value) return null;
81
+ return { value, label: field.label ?? field.displayValue ?? value };
82
+ })
83
+ .filter((opt): opt is PicklistOption => opt !== null);
84
+ }
@@ -0,0 +1,89 @@
1
+ import { X } from "lucide-react";
2
+ import { Button } from "../../../components/ui/button";
3
+ import { cn } from "../../../lib/utils";
4
+ import type { ActiveFilterValue } from "../utils/filterUtils";
5
+
6
+ function formatFilterLabel(filter: ActiveFilterValue): string {
7
+ const { label, type, value, min, max } = filter;
8
+
9
+ switch (type) {
10
+ case "search":
11
+ return `Search: ${value}`;
12
+ case "text":
13
+ case "picklist":
14
+ return `${label}: ${value}`;
15
+ case "multipicklist": {
16
+ const values = value ? value.split(",") : [];
17
+ if (values.length <= 2) return `${label}: ${values.join(", ")}`;
18
+ return `${label}: ${values.length} selected`;
19
+ }
20
+ case "boolean":
21
+ return `${label}: ${value === "true" ? "Yes" : "No"}`;
22
+ case "numeric": {
23
+ if (min && max) return `${label}: ${min} - ${max}`;
24
+ if (min) return `${label}: >= ${min}`;
25
+ return `${label}: <= ${max}`;
26
+ }
27
+ case "date": {
28
+ if (min && max) return `${label}: ${min} to ${max}`;
29
+ if (min) return `${label}: from ${min}`;
30
+ return `${label}: until ${max}`;
31
+ }
32
+ default:
33
+ return label;
34
+ }
35
+ }
36
+
37
+ interface ActiveFiltersProps extends React.ComponentProps<"div"> {
38
+ filters: ActiveFilterValue[];
39
+ onRemove: (field: string) => void;
40
+ buttonProps?: Omit<React.ComponentProps<typeof ActiveFilterButton>, "filter" | "onRemove">;
41
+ }
42
+
43
+ export function ActiveFilters({
44
+ filters,
45
+ onRemove,
46
+ className,
47
+ buttonProps,
48
+ ...props
49
+ }: ActiveFiltersProps) {
50
+ if (filters.length === 0) return null;
51
+
52
+ return (
53
+ <div className={cn("flex flex-wrap gap-2", className)} {...props}>
54
+ {filters.map((filter) => (
55
+ <ActiveFilterButton
56
+ key={filter.field}
57
+ filter={filter}
58
+ onRemove={onRemove}
59
+ {...buttonProps}
60
+ />
61
+ ))}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ interface ActiveFilterButtonProps extends React.ComponentProps<typeof Button> {
67
+ filter: ActiveFilterValue;
68
+ onRemove: (field: string) => void;
69
+ }
70
+
71
+ export function ActiveFilterButton({
72
+ filter,
73
+ onRemove,
74
+ className,
75
+ ...props
76
+ }: ActiveFilterButtonProps) {
77
+ return (
78
+ <Button
79
+ variant="outline"
80
+ size="sm"
81
+ className={cn("gap-1 h-7 text-xs", className)}
82
+ onClick={() => onRemove(filter.field)}
83
+ {...props}
84
+ >
85
+ {formatFilterLabel(filter)}
86
+ <X className="h-3 w-3" />
87
+ </Button>
88
+ );
89
+ }
@@ -0,0 +1,73 @@
1
+ import { createContext, useContext, useCallback, type ReactNode } from "react";
2
+ import { Button } from "../../../components/ui/button";
3
+ import type { ActiveFilterValue } from "../utils/filterUtils";
4
+
5
+ interface FilterContextValue {
6
+ filters: ActiveFilterValue[];
7
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
8
+ onFilterRemove: (field: string) => void;
9
+ onReset: () => void;
10
+ }
11
+
12
+ const FilterContext = createContext<FilterContextValue | null>(null);
13
+
14
+ interface FilterProviderProps {
15
+ filters: ActiveFilterValue[];
16
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
17
+ onFilterRemove: (field: string) => void;
18
+ onReset: () => void;
19
+ children: ReactNode;
20
+ }
21
+
22
+ export function FilterProvider({
23
+ filters,
24
+ onFilterChange,
25
+ onFilterRemove,
26
+ onReset,
27
+ children,
28
+ }: FilterProviderProps) {
29
+ return (
30
+ <FilterContext.Provider value={{ filters, onFilterChange, onFilterRemove, onReset }}>
31
+ {children}
32
+ </FilterContext.Provider>
33
+ );
34
+ }
35
+
36
+ function useFilterContext() {
37
+ const ctx = useContext(FilterContext);
38
+ if (!ctx) throw new Error("useFilterField must be used within a FilterProvider");
39
+ return ctx;
40
+ }
41
+
42
+ export function useFilterField(field: string) {
43
+ const { filters, onFilterChange, onFilterRemove } = useFilterContext();
44
+ const value = filters.find((f) => f.field === field);
45
+ const onChange = useCallback(
46
+ (next: ActiveFilterValue | undefined) => {
47
+ if (next) {
48
+ onFilterChange(field, next);
49
+ } else {
50
+ onFilterRemove(field);
51
+ }
52
+ },
53
+ [field, onFilterChange, onFilterRemove],
54
+ );
55
+ return { value, onChange };
56
+ }
57
+
58
+ export function useFilterPanel() {
59
+ const { filters, onReset } = useFilterContext();
60
+ return { hasActiveFilters: filters.length > 0, resetAll: onReset };
61
+ }
62
+
63
+ interface FilterResetButtonProps extends Omit<React.ComponentProps<typeof Button>, "onClick"> {}
64
+
65
+ export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
66
+ const { hasActiveFilters, resetAll } = useFilterPanel();
67
+ if (!hasActiveFilters) return null;
68
+ return (
69
+ <Button onClick={resetAll} aria-label="Reset filters" variant="destructive" {...props}>
70
+ {children ?? "Reset"}
71
+ </Button>
72
+ );
73
+ }
@@ -0,0 +1,66 @@
1
+ import { Link } from "react-router";
2
+ import {
3
+ Breadcrumb,
4
+ BreadcrumbList,
5
+ BreadcrumbItem,
6
+ BreadcrumbLink,
7
+ BreadcrumbSeparator,
8
+ BreadcrumbPage,
9
+ } from "../../../components/ui/breadcrumb";
10
+ import { Skeleton } from "../../../components/ui/skeleton";
11
+
12
+ interface ObjectBreadcrumbProps {
13
+ listPath: string;
14
+ listLabel: string;
15
+ recordName?: string;
16
+ loading?: boolean;
17
+ includeHome?: boolean; // default is true
18
+ homeLabel?: string; // default is "Home"
19
+ }
20
+
21
+ export function ObjectBreadcrumb({
22
+ listPath,
23
+ listLabel,
24
+ recordName,
25
+ loading,
26
+ includeHome = true,
27
+ homeLabel = "Home",
28
+ }: ObjectBreadcrumbProps) {
29
+ const isDetailView = loading || recordName;
30
+
31
+ return (
32
+ <Breadcrumb className="mb-3">
33
+ <BreadcrumbList>
34
+ {includeHome && (
35
+ <BreadcrumbItem>
36
+ <BreadcrumbLink asChild>
37
+ <Link to="/">{homeLabel}</Link>
38
+ </BreadcrumbLink>
39
+ </BreadcrumbItem>
40
+ )}
41
+ <BreadcrumbSeparator />
42
+ {isDetailView ? (
43
+ <>
44
+ <BreadcrumbItem>
45
+ <BreadcrumbLink asChild>
46
+ <Link to={listPath}>{listLabel}</Link>
47
+ </BreadcrumbLink>
48
+ </BreadcrumbItem>
49
+ <BreadcrumbSeparator />
50
+ <BreadcrumbItem>
51
+ {loading && !recordName ? (
52
+ <Skeleton className="h-4 w-32" />
53
+ ) : (
54
+ <BreadcrumbPage>{recordName}</BreadcrumbPage>
55
+ )}
56
+ </BreadcrumbItem>
57
+ </>
58
+ ) : (
59
+ <BreadcrumbItem>
60
+ <BreadcrumbPage>{listLabel}</BreadcrumbPage>
61
+ </BreadcrumbItem>
62
+ )}
63
+ </BreadcrumbList>
64
+ </Breadcrumb>
65
+ );
66
+ }
@@ -0,0 +1,109 @@
1
+ import {
2
+ Pagination,
3
+ PaginationContent,
4
+ PaginationItem,
5
+ PaginationPrevious,
6
+ PaginationNext,
7
+ } from "../../../components/ui/pagination";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "../../../components/ui/select";
15
+ import { Label } from "../../../components/ui/label";
16
+
17
+ interface PaginationControlsProps {
18
+ pageIndex: number;
19
+ hasNextPage: boolean;
20
+ hasPreviousPage: boolean;
21
+ pageSize: number;
22
+ pageSizeOptions: readonly number[];
23
+ onNextPage: () => void;
24
+ onPreviousPage: () => void;
25
+ onPageSizeChange: (newPageSize: number) => void;
26
+ disabled?: boolean;
27
+ }
28
+
29
+ export default function PaginationControls({
30
+ pageIndex,
31
+ hasNextPage,
32
+ hasPreviousPage,
33
+ pageSize,
34
+ pageSizeOptions,
35
+ onNextPage,
36
+ onPreviousPage,
37
+ onPageSizeChange,
38
+ disabled = false,
39
+ }: PaginationControlsProps) {
40
+ const handlePageSizeChange = (newValue: string) => {
41
+ const newSize = parseInt(newValue, 10);
42
+ if (!isNaN(newSize) && newSize !== pageSize) {
43
+ onPageSizeChange(newSize);
44
+ }
45
+ };
46
+ const currentPage = pageIndex + 1;
47
+ const prevDisabled = disabled || !hasPreviousPage;
48
+ const nextDisabled = disabled || !hasNextPage;
49
+
50
+ return (
51
+ <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
52
+ <div
53
+ className="flex justify-center sm:justify-start items-center gap-2 shrink-0 row-2 sm:row-1"
54
+ role="group"
55
+ aria-label="Page size selector"
56
+ >
57
+ <Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
58
+ Results per page:
59
+ </Label>
60
+ <Select
61
+ value={pageSize.toString()}
62
+ onValueChange={handlePageSizeChange}
63
+ disabled={disabled}
64
+ >
65
+ <SelectTrigger
66
+ id="page-size-select"
67
+ className="w-16"
68
+ aria-label="Select number of results per page"
69
+ >
70
+ <SelectValue />
71
+ </SelectTrigger>
72
+ <SelectContent>
73
+ {pageSizeOptions.map((size) => (
74
+ <SelectItem key={size} value={size.toString()}>
75
+ {size}
76
+ </SelectItem>
77
+ ))}
78
+ </SelectContent>
79
+ </Select>
80
+ </div>
81
+ <Pagination className="w-full mx-0 sm:justify-end">
82
+ <PaginationContent>
83
+ <PaginationItem>
84
+ <PaginationPrevious
85
+ onClick={prevDisabled ? undefined : onPreviousPage}
86
+ aria-disabled={prevDisabled}
87
+ className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
88
+ />
89
+ </PaginationItem>
90
+ <PaginationItem>
91
+ <span
92
+ className="min-w-16 text-center text-sm text-muted-foreground px-2"
93
+ aria-current="page"
94
+ >
95
+ Page {currentPage}
96
+ </span>
97
+ </PaginationItem>
98
+ <PaginationItem>
99
+ <PaginationNext
100
+ onClick={nextDisabled ? undefined : onNextPage}
101
+ aria-disabled={nextDisabled}
102
+ className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
103
+ />
104
+ </PaginationItem>
105
+ </PaginationContent>
106
+ </Pagination>
107
+ </div>
108
+ );
109
+ }
@@ -0,0 +1,41 @@
1
+ import { Search } from "lucide-react";
2
+ import { Input } from "../../../components/ui/input";
3
+ import { cn } from "../../../lib/utils";
4
+
5
+ interface SearchBarProps extends React.ComponentProps<"div"> {
6
+ value: string;
7
+ handleChange: (value: string) => void;
8
+ placeholder?: string;
9
+ iconProps?: React.ComponentProps<typeof Search>;
10
+ inputProps?: Omit<React.ComponentProps<typeof Input>, "value">;
11
+ }
12
+
13
+ export function SearchBar({
14
+ value,
15
+ handleChange,
16
+ placeholder,
17
+ className,
18
+ iconProps,
19
+ inputProps,
20
+ ...props
21
+ }: SearchBarProps) {
22
+ return (
23
+ <div className={cn("relative flex-1", className)} title={placeholder} {...props}>
24
+ <Search
25
+ {...iconProps}
26
+ className={cn(
27
+ "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground",
28
+ iconProps?.className,
29
+ )}
30
+ />
31
+ <Input
32
+ type="text"
33
+ value={value}
34
+ onChange={(e) => handleChange(e.target.value)}
35
+ placeholder={placeholder}
36
+ {...inputProps}
37
+ className={cn("pl-9", inputProps?.className)}
38
+ />
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,143 @@
1
+ import { ArrowUp, ArrowDown } from "lucide-react";
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "../../../components/ui/select";
9
+ import { Button } from "../../../components/ui/button";
10
+ import { cn } from "../../../lib/utils";
11
+ import type { SortFieldConfig, SortState } from "../utils/sortUtils";
12
+
13
+ const NONE_VALUE = "__none__";
14
+
15
+ interface SortControlProps extends React.ComponentProps<"div"> {
16
+ configs: SortFieldConfig[];
17
+ sort: SortState | null;
18
+ onSortChange: (sort: SortState | null) => void;
19
+ labelProps?: React.ComponentProps<"span">;
20
+ selectProps?: Omit<
21
+ React.ComponentProps<typeof SortControlSelect>,
22
+ "configs" | "sort" | "onSortChange"
23
+ >;
24
+ directionButtonProps?: Omit<
25
+ React.ComponentProps<typeof SortDirectionButton>,
26
+ "sort" | "onSortChange"
27
+ >;
28
+ }
29
+
30
+ export function SortControl({
31
+ configs,
32
+ sort,
33
+ onSortChange,
34
+ className,
35
+ labelProps,
36
+ selectProps,
37
+ directionButtonProps,
38
+ ...props
39
+ }: SortControlProps) {
40
+ return (
41
+ <div className={cn("flex items-center gap-2", className)} {...props}>
42
+ <span
43
+ {...labelProps}
44
+ className={cn("text-sm text-muted-foreground whitespace-nowrap", labelProps?.className)}
45
+ >
46
+ {labelProps?.children ?? "Sort by"}
47
+ </span>
48
+ <SortControlSelect
49
+ configs={configs}
50
+ sort={sort}
51
+ onSortChange={onSortChange}
52
+ {...selectProps}
53
+ />
54
+ {sort && (
55
+ <SortDirectionButton sort={sort} onSortChange={onSortChange} {...directionButtonProps} />
56
+ )}
57
+ </div>
58
+ );
59
+ }
60
+
61
+ interface SortControlSelectProps {
62
+ configs: SortFieldConfig[];
63
+ sort: SortState | null;
64
+ onSortChange: (sort: SortState | null) => void;
65
+ triggerProps?: React.ComponentProps<typeof SelectTrigger>;
66
+ contentProps?: React.ComponentProps<typeof SelectContent>;
67
+ selectValueProps?: React.ComponentProps<typeof SelectValue>;
68
+ selectItemProps?: Omit<React.ComponentProps<typeof SelectItem>, "value">;
69
+ }
70
+
71
+ export function SortControlSelect({
72
+ configs,
73
+ sort,
74
+ onSortChange,
75
+ triggerProps,
76
+ contentProps,
77
+ selectValueProps,
78
+ selectItemProps,
79
+ }: SortControlSelectProps) {
80
+ return (
81
+ <Select
82
+ value={sort?.field ?? NONE_VALUE}
83
+ onValueChange={(v) => {
84
+ if (v === NONE_VALUE) {
85
+ onSortChange(null);
86
+ } else {
87
+ onSortChange({
88
+ field: v,
89
+ direction: sort?.direction ?? "ASC",
90
+ });
91
+ }
92
+ }}
93
+ >
94
+ <SelectTrigger
95
+ size="sm"
96
+ {...triggerProps}
97
+ className={cn("w-[160px]", triggerProps?.className)}
98
+ >
99
+ <SelectValue placeholder="Default" {...selectValueProps} />
100
+ </SelectTrigger>
101
+ <SelectContent {...contentProps}>
102
+ <SelectItem value={NONE_VALUE} {...selectItemProps}>
103
+ Default
104
+ </SelectItem>
105
+ {configs.map((c) => (
106
+ <SelectItem key={c.field} value={c.field} {...selectItemProps}>
107
+ {c.label}
108
+ </SelectItem>
109
+ ))}
110
+ </SelectContent>
111
+ </Select>
112
+ );
113
+ }
114
+
115
+ interface SortDirectionButtonProps extends React.ComponentProps<typeof Button> {
116
+ sort: SortState;
117
+ onSortChange: (sort: SortState) => void;
118
+ }
119
+
120
+ export function SortDirectionButton({
121
+ sort,
122
+ onSortChange,
123
+ className,
124
+ ...props
125
+ }: SortDirectionButtonProps) {
126
+ return (
127
+ <Button
128
+ variant="ghost"
129
+ size="icon-sm"
130
+ className={cn(className)}
131
+ onClick={() =>
132
+ onSortChange({
133
+ ...sort,
134
+ direction: sort.direction === "ASC" ? "DESC" : "ASC",
135
+ })
136
+ }
137
+ aria-label={`Sort ${sort.direction === "ASC" ? "descending" : "ascending"}`}
138
+ {...props}
139
+ >
140
+ {sort.direction === "ASC" ? <ArrowUp /> : <ArrowDown />}
141
+ </Button>
142
+ );
143
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "../../../../components/ui/select";
8
+ import { Label } from "../../../../components/ui/label";
9
+ import { cn } from "../../../../lib/utils";
10
+ import { useFilterField } from "../FilterContext";
11
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
12
+
13
+ const ALL_VALUE = "__all__";
14
+
15
+ interface BooleanFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
16
+ field: string;
17
+ label: string;
18
+ helpText?: string;
19
+ }
20
+
21
+ export function BooleanFilter({ field, label, helpText, className, ...props }: BooleanFilterProps) {
22
+ const { value, onChange } = useFilterField(field);
23
+ return (
24
+ <div className={cn("space-y-1.5", className)} {...props}>
25
+ <Label htmlFor={`filter-${field}`}>{label}</Label>
26
+ <BooleanFilterSelect field={field} label={label} value={value} onChange={onChange} />
27
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ interface BooleanFilterSelectProps {
33
+ field: string;
34
+ label: string;
35
+ value: ActiveFilterValue | undefined;
36
+ onChange: (value: ActiveFilterValue | undefined) => void;
37
+ triggerProps?: React.ComponentProps<typeof SelectTrigger>;
38
+ contentProps?: React.ComponentProps<typeof SelectContent>;
39
+ }
40
+
41
+ export function BooleanFilterSelect({
42
+ field,
43
+ label,
44
+ value,
45
+ onChange,
46
+ triggerProps,
47
+ contentProps,
48
+ }: BooleanFilterSelectProps) {
49
+ return (
50
+ <Select
51
+ value={value?.value ?? ALL_VALUE}
52
+ onValueChange={(v) => {
53
+ if (v === ALL_VALUE) {
54
+ onChange(undefined);
55
+ } else {
56
+ onChange({ field, label, type: "boolean", value: v });
57
+ }
58
+ }}
59
+ >
60
+ <SelectTrigger
61
+ id={`filter-${field}`}
62
+ {...triggerProps}
63
+ className={cn("w-full", triggerProps?.className)}
64
+ >
65
+ <SelectValue />
66
+ </SelectTrigger>
67
+ <SelectContent {...contentProps}>
68
+ <SelectItem value={ALL_VALUE}>All</SelectItem>
69
+ <SelectItem value="true">Yes</SelectItem>
70
+ <SelectItem value="false">No</SelectItem>
71
+ </SelectContent>
72
+ </Select>
73
+ );
74
+ }