@salesforce/webapp-template-app-react-template-b2x-experimental 1.109.5 → 1.109.7

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 (104) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/package.json +4 -3
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/graphql-operations-types.ts +11260 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/ui/sonner.tsx +20 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/authentication/sessionTimeout/SessionTimeoutValidator.tsx +8 -16
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/authentication/sessionTimeout/sessionTimeService.ts +30 -42
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +275 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/FilterPanel.tsx +127 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/PaginationControls.tsx +151 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SortControl.tsx +143 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/BooleanFilter.tsx +94 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateFilter.tsx +138 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +78 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +106 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +102 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SearchFilter.tsx +40 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/TextFilter.tsx +77 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useAsyncData.ts +53 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useCachedAsyncData.ts +183 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useObjectSearchParams.ts +225 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/debounce.ts +22 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  37. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/Home.tsx +10 -11
  38. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/routes.tsx +8 -20
  39. package/dist/package-lock.json +2 -2
  40. package/dist/package.json +1 -1
  41. package/package.json +1 -1
  42. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  43. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  44. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  45. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  46. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  47. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  48. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  49. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  50. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  51. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  52. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  53. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  54. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  55. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  56. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  57. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  58. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  59. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  60. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchPagination.tsx +0 -144
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  72. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/constants.ts +0 -39
  73. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  74. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  75. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/form.tsx +0 -209
  76. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  77. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  78. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  79. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  80. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  81. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  82. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/filters.ts +0 -121
  83. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  84. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  85. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  86. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/schema.d.ts +0 -200
  87. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/search/searchResults.ts +0 -229
  88. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  89. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  90. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/debounce.ts +0 -90
  91. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  92. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  93. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  94. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  95. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formUtils.ts +0 -142
  96. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  97. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  98. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  99. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  100. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  101. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  102. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  103. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  104. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/index.ts +0 -120
@@ -1,146 +0,0 @@
1
- import { useState, useCallback, useMemo, useId } from "react";
2
- import type { LayoutResponse } from "../../types/recordDetail/recordDetail";
3
- import type { GraphQLRecordNode } from "../../api/recordListGraphQLService";
4
- import {
5
- getDisplayValueForLayoutItemFromNode,
6
- getDisplayValueForDetailFieldFromNode,
7
- } from "../../utils/graphQLNodeFieldUtils";
8
- import type { ObjectInfoMetadata } from "../../utils/formDataTransformUtils";
9
- import {
10
- getTransformedSections,
11
- type LayoutTransformContext,
12
- type ObjectInfo,
13
- type PicklistOption,
14
- type TransformedLayoutItem,
15
- } from "../../utils/layoutTransformUtils";
16
- import { FieldValueDisplay } from "./formatted/FieldValueDisplay";
17
- import { Section } from "./Section";
18
- import { SectionRow } from "./SectionRow";
19
-
20
- export interface DetailFormProps {
21
- layout: LayoutResponse;
22
- record: GraphQLRecordNode;
23
- metadata?: ObjectInfoMetadata | null;
24
- objectInfo?: ObjectInfo | null;
25
- lookupRecords?: Record<string, PicklistOption[] | null> | null;
26
- showSectionHeaders?: boolean;
27
- collapsibleSections?: boolean;
28
- }
29
-
30
- function FieldCell({
31
- item,
32
- record,
33
- metadata,
34
- }: {
35
- item: TransformedLayoutItem;
36
- record: GraphQLRecordNode;
37
- metadata?: ObjectInfoMetadata | null;
38
- }) {
39
- const labelId = useId();
40
- const valueId = useId();
41
- if (!item.isField || item.apiName == null) return null;
42
- const label = item.label ?? item.apiName;
43
- const hasComponents = item.layoutComponentApiNames && item.layoutComponentApiNames.length > 0;
44
- const layoutResult = hasComponents
45
- ? getDisplayValueForLayoutItemFromNode(
46
- record,
47
- item.layoutComponentApiNames as string[],
48
- metadata,
49
- )
50
- : null;
51
- const value = hasComponents
52
- ? (layoutResult?.value ?? null)
53
- : getDisplayValueForDetailFieldFromNode(record, item.apiName, metadata);
54
- const dataType =
55
- (hasComponents ? layoutResult?.dataType : undefined) ?? item.dataType ?? undefined;
56
- return (
57
- <div
58
- className="flex flex-col gap-1"
59
- role="group"
60
- aria-labelledby={labelId}
61
- aria-describedby={valueId}
62
- >
63
- <dt id={labelId} className="text-sm font-medium text-muted-foreground">
64
- {label}
65
- </dt>
66
- <dd id={valueId} className="text-sm text-foreground">
67
- <FieldValueDisplay value={value} dataType={dataType} />
68
- </dd>
69
- </div>
70
- );
71
- }
72
-
73
- /**
74
- * Read-only detail form: layout API + record (+ optional object info) drive sections, rows, and
75
- * field values. Uses layoutComponents to club multi-component items (address, Created By, etc.).
76
- */
77
- export function DetailForm({
78
- layout,
79
- record,
80
- metadata = null,
81
- objectInfo = null,
82
- lookupRecords = null,
83
- showSectionHeaders = true,
84
- collapsibleSections = true,
85
- }: DetailFormProps) {
86
- const [collapsedSections, setCollapsedSections] = useState<Record<string, boolean>>({});
87
-
88
- const recordId = (record.Id as string) ?? "";
89
-
90
- const layoutObjectInfo = objectInfo ?? metadata;
91
-
92
- const transformContext: LayoutTransformContext = useMemo(
93
- () => ({
94
- recordId,
95
- objectInfo: layoutObjectInfo,
96
- lookupRecords,
97
- getSectionCollapsedState: (sectionId: string) => Boolean(collapsedSections[sectionId]),
98
- }),
99
- [recordId, layoutObjectInfo, lookupRecords, collapsedSections],
100
- );
101
-
102
- const computedSections = useMemo(
103
- () => getTransformedSections(layout.sections, transformContext),
104
- [layout.sections, transformContext],
105
- );
106
-
107
- const handleSectionToggle = useCallback((sectionId: string, collapsed: boolean) => {
108
- setCollapsedSections((prev) => ({ ...prev, [sectionId]: collapsed }));
109
- }, []);
110
-
111
- return (
112
- <div
113
- className="space-y-6"
114
- role="region"
115
- aria-label="Record details"
116
- aria-roledescription="Detail form"
117
- >
118
- {computedSections.map((section) => (
119
- <Section
120
- key={section.key}
121
- sectionId={section.id}
122
- titleLabel={section.heading}
123
- showHeader={showSectionHeaders && section.useHeading}
124
- collapsible={collapsibleSections && section.collapsible}
125
- collapsed={section.collapsed}
126
- onToggle={handleSectionToggle}
127
- >
128
- <div className="space-y-4">
129
- {section.layoutRows.map((row) => (
130
- <SectionRow key={row.key}>
131
- {row.layoutItems.map((item) => {
132
- const cellKey = `${section.key}-${row.key}-${item.apiName ?? item.key}`;
133
- return item.isField ? (
134
- <FieldCell key={cellKey} item={item} record={record} metadata={metadata} />
135
- ) : (
136
- <div key={cellKey} className="min-h-[2.5rem]" aria-hidden="true" />
137
- );
138
- })}
139
- </SectionRow>
140
- ))}
141
- </div>
142
- </Section>
143
- ))}
144
- </div>
145
- );
146
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * Back button and title for the record detail page.
3
- *
4
- * @param title - Record title (e.g. record name) shown next to the back control.
5
- * @param onBack - Called when the user activates the back control.
6
- */
7
- import { Button } from "../../../../components/ui/button";
8
- import { ArrowLeft } from "lucide-react";
9
-
10
- interface DetailHeaderProps {
11
- title: string;
12
- onBack: () => void;
13
- }
14
-
15
- export default function DetailHeader({ title, onBack }: DetailHeaderProps) {
16
- return (
17
- <div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
18
- <Button
19
- variant="ghost"
20
- onClick={onBack}
21
- className="w-fit"
22
- aria-label="Go back to search results"
23
- >
24
- <ArrowLeft className="h-4 w-4 mr-2" aria-hidden="true" />
25
- Back
26
- </Button>
27
- {title ? (
28
- <h1 className="text-xl font-semibold text-foreground truncate" id="detail-page-title">
29
- {title}
30
- </h1>
31
- ) : null}
32
- </div>
33
- );
34
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Alternative detail rendering: layout sections → rows → items → label/value grid.
3
- *
4
- * Use when you have raw Layout API response + record and do not need the full
5
- * layoutTransformUtils + formDataTransformUtils pipeline. The primary detail view
6
- * (DetailPage) uses DetailForm via UiApiDetailForm; use this component for other
7
- * entry points that already have layout + record in hand.
8
- */
9
- import type { LayoutResponse } from "../../types/recordDetail/recordDetail";
10
- import type { SearchResultRecordData } from "../../types/search/searchResults";
11
- import { getNestedFieldValue } from "../../utils/fieldUtils";
12
-
13
- interface DetailLayoutSectionsProps {
14
- layout: LayoutResponse;
15
- record: SearchResultRecordData;
16
- }
17
-
18
- interface FieldEntry {
19
- key: string;
20
- label: string;
21
- value: string | number | boolean | null;
22
- }
23
-
24
- function getSectionFieldEntries(
25
- section: LayoutResponse["sections"][number],
26
- record: SearchResultRecordData,
27
- ): FieldEntry[] {
28
- const entries: FieldEntry[] = [];
29
- section.layoutRows.forEach((row, rowIdx) => {
30
- row.layoutItems.forEach((item, itemIdx) => {
31
- item.layoutComponents.forEach((comp, compIdx) => {
32
- if (comp.componentType !== "Field" || !comp.apiName) return;
33
- const value = getNestedFieldValue(record.fields, comp.apiName);
34
- const label = comp.label ?? item.label;
35
- entries.push({
36
- key: `${section.id}-${rowIdx}-${itemIdx}-${comp.apiName ?? compIdx}`,
37
- label: label || comp.apiName,
38
- value: value ?? null,
39
- });
40
- });
41
- });
42
- });
43
- return entries;
44
- }
45
-
46
- export default function DetailLayoutSections({ layout, record }: DetailLayoutSectionsProps) {
47
- return (
48
- <div className="space-y-8" role="region" aria-label="Record details">
49
- {layout.sections.map((section) => {
50
- const entries = getSectionFieldEntries(section, record);
51
- if (entries.length === 0) return null;
52
-
53
- return (
54
- <section
55
- key={section.id}
56
- className="space-y-4"
57
- aria-labelledby={section.useHeading ? `section-${section.id}` : undefined}
58
- >
59
- {section.useHeading && section.heading ? (
60
- <h3
61
- id={`section-${section.id}`}
62
- className="text-base font-semibold text-foreground border-b pb-2"
63
- >
64
- {section.heading}
65
- </h3>
66
- ) : null}
67
- <dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4">
68
- {entries.map(({ key, label, value }) => (
69
- <div key={key} className="flex flex-col gap-1">
70
- <dt className="text-sm font-medium text-muted-foreground">{label}</dt>
71
- <dd className="text-sm text-foreground">{value || "—"}</dd>
72
- </div>
73
- ))}
74
- </dl>
75
- </section>
76
- );
77
- })}
78
- </div>
79
- );
80
- }
@@ -1,108 +0,0 @@
1
- import { useState, useCallback, useEffect, useRef } from "react";
2
- import { ChevronDown, ChevronRight } from "lucide-react";
3
- import type { ReactNode } from "react";
4
-
5
- export interface SectionProps {
6
- sectionId: string;
7
- titleLabel: string;
8
- showHeader: boolean;
9
- collapsible: boolean;
10
- /** When provided, section is controlled (parent owns state). When undefined, section is uncontrolled (internal state). */
11
- collapsed?: boolean;
12
- onToggle?: (sectionId: string, collapsed: boolean) => void;
13
- children: ReactNode;
14
- }
15
-
16
- /**
17
- * Section block with optional heading and collapsible content. Controlled when
18
- * `collapsed` is passed; uncontrolled otherwise. Accessible: aria-expanded, aria-controls, keyboard (Enter/Space).
19
- */
20
- export function Section({
21
- sectionId,
22
- titleLabel,
23
- showHeader,
24
- collapsible,
25
- collapsed: controlledCollapsed,
26
- onToggle,
27
- children,
28
- }: SectionProps) {
29
- const [internalCollapsed, setInternalCollapsed] = useState(false);
30
- const isControlled = controlledCollapsed !== undefined;
31
- const collapsed = isControlled ? controlledCollapsed : internalCollapsed;
32
-
33
- const warnedUncontrolledRef = useRef(false);
34
- useEffect(() => {
35
- if (
36
- process.env.NODE_ENV === "development" &&
37
- onToggle != null &&
38
- !isControlled &&
39
- !warnedUncontrolledRef.current
40
- ) {
41
- warnedUncontrolledRef.current = true;
42
- console.warn(
43
- "[Section] onToggle is passed but collapsed is undefined; section is uncontrolled. Pass collapsed to control from parent.",
44
- );
45
- }
46
- }, [onToggle, isControlled]);
47
-
48
- const contentId = `section-content-${sectionId}`;
49
- const headerId = `section-header-${sectionId}`;
50
-
51
- const handleToggle = useCallback(() => {
52
- const next = !collapsed;
53
- if (!isControlled) setInternalCollapsed(next);
54
- onToggle?.(sectionId, next);
55
- }, [collapsed, isControlled, onToggle, sectionId]);
56
-
57
- const handleKeyDown = useCallback(
58
- (e: React.KeyboardEvent) => {
59
- if (!collapsible) return;
60
- if (e.key === "Enter" || e.key === " ") {
61
- e.preventDefault();
62
- handleToggle();
63
- }
64
- },
65
- [collapsible, handleToggle],
66
- );
67
-
68
- return (
69
- <section
70
- className="border-b border-border last:border-b-0 pb-6 last:pb-0"
71
- aria-labelledby={showHeader ? headerId : undefined}
72
- >
73
- {showHeader && titleLabel && (
74
- <h3 id={headerId} className="text-base font-semibold text-foreground mb-4">
75
- {collapsible ? (
76
- <button
77
- type="button"
78
- className="flex items-center gap-2 w-full text-left hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded"
79
- onClick={handleToggle}
80
- onKeyDown={handleKeyDown}
81
- aria-expanded={!collapsed}
82
- aria-controls={contentId}
83
- aria-label={`${titleLabel}, ${collapsed ? "expand" : "collapse"} section`}
84
- aria-roledescription="Section toggle"
85
- >
86
- {collapsed ? (
87
- <ChevronRight className="h-4 w-4 shrink-0" aria-hidden />
88
- ) : (
89
- <ChevronDown className="h-4 w-4 shrink-0" aria-hidden />
90
- )}
91
- <span>{titleLabel}</span>
92
- </button>
93
- ) : (
94
- <span className="block">{titleLabel}</span>
95
- )}
96
- </h3>
97
- )}
98
- <div
99
- id={contentId}
100
- className={showHeader && collapsible ? "mt-2" : ""}
101
- aria-hidden={collapsible ? collapsed : undefined}
102
- hidden={collapsible && collapsed}
103
- >
104
- {children}
105
- </div>
106
- </section>
107
- );
108
- }
@@ -1,20 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- export interface SectionRowProps {
4
- children: ReactNode;
5
- }
6
-
7
- /**
8
- * One row of the detail form: definition list (dl) with two-column grid. Each child
9
- * is a layout item (field cell or placeholder) from the layout API row.
10
- */
11
- export function SectionRow({ children }: SectionRowProps) {
12
- return (
13
- <dl
14
- className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4 sm:gap-y-2"
15
- aria-label="Row of fields"
16
- >
17
- {children}
18
- </dl>
19
- );
20
- }
@@ -1,140 +0,0 @@
1
- import { useEffect, useMemo } from "react";
2
- import type { LayoutResponse } from "../../types/recordDetail/recordDetail";
3
- import { useRecordDetailLayout } from "../../hooks/useRecordDetailLayout";
4
- import { toRecordDisplayNameMetadata } from "../../utils/fieldUtils";
5
- import { DetailForm } from "./DetailForm";
6
- import type { ObjectInfoResult } from "../../types/objectInfo/objectInfo";
7
- import type { ObjectInfoMetadata } from "../../utils/formDataTransformUtils";
8
- import type { ObjectInfo } from "../../utils/layoutTransformUtils";
9
- import type { GraphQLRecordNode } from "../../api/recordListGraphQLService";
10
- import { getGraphQLRecordDisplayName } from "../../utils/graphQLNodeFieldUtils";
11
-
12
- export interface UiApiDetailFormProps {
13
- objectApiName: string;
14
- recordId: string;
15
- recordTypeId?: string | null;
16
- /** When provided, skips fetching and uses this layout (controlled mode). */
17
- layout?: LayoutResponse | null;
18
- /** When provided with layout, skips fetching and uses this record (controlled mode). */
19
- record?: GraphQLRecordNode | null;
20
- /** When provided, skips fetching and uses this object metadata (controlled mode). */
21
- objectMetadata?: ObjectInfoResult | null;
22
- /** When true, shows a loading spinner until layout and record are ready. */
23
- loadsWithSpinner?: boolean;
24
- /** Reserved for future edit mode; no-op in read-only. */
25
- hideFooter?: boolean;
26
- /** Callback when layout and record are ready (e.g. for parent to show record title). */
27
- onRecordDataUpdate?: (payload: { recordName: string; record: unknown }) => void;
28
- }
29
-
30
- /**
31
- * Entry component for the record detail view. When layout/record are not provided,
32
- * fetches them via useRecordDetailLayout. Shows optional loading spinner and renders
33
- * a read-only DetailForm when ready. Mirrors LWC uiApiDetailForm (read-only).
34
- *
35
- * Passes objectInfo (mapped from object metadata) to DetailForm for layout transform.
36
- * lookupRecords (picklist/lookup options) are not fetched in this flow; DetailForm
37
- * accepts them when provided (e.g. from a future picklist API). Omit for read-only
38
- * display without API-driven picklist labels.
39
- */
40
- export function UiApiDetailForm({
41
- objectApiName,
42
- recordId,
43
- recordTypeId = null,
44
- layout: layoutProp,
45
- record: recordProp,
46
- objectMetadata: objectMetadataProp,
47
- loadsWithSpinner = false,
48
- onRecordDataUpdate,
49
- }: UiApiDetailFormProps) {
50
- // Memoize so hook dependency doesn't change every render (avoids duplicate fetches)
51
- const initialData = useMemo(
52
- () =>
53
- layoutProp && recordProp && objectMetadataProp
54
- ? {
55
- layout: layoutProp,
56
- record: recordProp,
57
- objectMetadata: objectMetadataProp,
58
- }
59
- : null,
60
- [layoutProp, recordProp, objectMetadataProp],
61
- );
62
-
63
- const fetched = useRecordDetailLayout({
64
- objectApiName,
65
- recordId,
66
- recordTypeId,
67
- initialData,
68
- });
69
-
70
- const layout = layoutProp ?? fetched.layout;
71
- const record = recordProp ?? fetched.record;
72
- const metadata = objectMetadataProp ?? fetched.objectMetadata;
73
- const loading = layoutProp == null || recordProp == null ? fetched.loading : false;
74
- const error = layoutProp == null || recordProp == null ? fetched.error : null;
75
-
76
- const objectInfo: ObjectInfo | null = useMemo(() => {
77
- if (!metadata?.fields) return null;
78
- const apiName = metadata.ApiName;
79
- return {
80
- apiName,
81
- fields: Object.fromEntries(
82
- Object.entries(metadata.fields).map(([name, f]) => [
83
- name,
84
- {
85
- compoundFieldName: f.compoundFieldName ?? undefined,
86
- dataType: f.dataType ?? "",
87
- },
88
- ]),
89
- ),
90
- };
91
- }, [metadata]);
92
-
93
- const isReadyToRender = Boolean(layout && record && layout.sections?.length);
94
-
95
- const showSpinner = !isReadyToRender && loadsWithSpinner && loading;
96
-
97
- useEffect(() => {
98
- if (!record || !onRecordDataUpdate || !isReadyToRender) return;
99
- onRecordDataUpdate({
100
- recordName: getGraphQLRecordDisplayName(record, toRecordDisplayNameMetadata(metadata)),
101
- record,
102
- });
103
- }, [record, metadata, onRecordDataUpdate, isReadyToRender]);
104
-
105
- if (showSpinner) {
106
- return (
107
- <div
108
- className="min-h-[80px] flex items-center justify-center"
109
- role="status"
110
- aria-live="polite"
111
- aria-label="Loading record details"
112
- >
113
- <span className="sr-only">Loading record details</span>
114
- <div
115
- className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent"
116
- aria-hidden
117
- />
118
- </div>
119
- );
120
- }
121
-
122
- if (error || !layout || !record) {
123
- return null;
124
- }
125
-
126
- if (!isReadyToRender) {
127
- return null;
128
- }
129
-
130
- return (
131
- <DetailForm
132
- layout={layout}
133
- record={record}
134
- metadata={metadata as ObjectInfoMetadata}
135
- objectInfo={objectInfo}
136
- showSectionHeaders
137
- collapsibleSections
138
- />
139
- );
140
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Picks formatter by dataType (Address, Phone, Url, Email) or plain text. Empty values show "—" with a11y label.
3
- */
4
- import { FormattedAddress } from "./FormattedAddress";
5
- import { FormattedEmail } from "./FormattedEmail";
6
- import { FormattedPhone } from "./FormattedPhone";
7
- import { FormattedText } from "./FormattedText";
8
- import { FormattedUrl } from "./FormattedUrl";
9
-
10
- /** Salesforce UI API dataType values that have dedicated formatters. */
11
- const DATA_TYPES = {
12
- Address: "Address",
13
- Email: "Email",
14
- Phone: "Phone",
15
- Url: "Url",
16
- } as const;
17
-
18
- /** Normalize dataType to canonical casing so "PHONE" / "phone" match Phone, etc. */
19
- function normalizeDataType(dataType: string | undefined): string | undefined {
20
- if (dataType == null || dataType === "") return dataType;
21
- const lower = dataType.toLowerCase();
22
- const canonical: Record<string, string> = {
23
- phone: DATA_TYPES.Phone,
24
- email: DATA_TYPES.Email,
25
- url: DATA_TYPES.Url,
26
- address: DATA_TYPES.Address,
27
- };
28
- return canonical[lower] ?? dataType;
29
- }
30
-
31
- export interface FieldValueDisplayProps {
32
- /** Resolved display value (string, number, boolean, or null). */
33
- value: string | number | boolean | null | undefined;
34
- /** Field dataType from object info (e.g. Phone, Email, Url, Address). */
35
- dataType?: string;
36
- className?: string;
37
- }
38
-
39
- const DEFAULT_CLASS = "text-sm text-foreground";
40
- const LINK_CLASS =
41
- "text-sm text-foreground text-primary underline underline-offset-2 hover:opacity-80";
42
-
43
- export function FieldValueDisplay({
44
- value,
45
- dataType,
46
- className = DEFAULT_CLASS,
47
- }: FieldValueDisplayProps) {
48
- const str = value || null;
49
-
50
- if (str === null) {
51
- return (
52
- <span className={className} aria-label="No value">
53
-
54
- </span>
55
- );
56
- }
57
-
58
- const linkClassName = className === DEFAULT_CLASS ? LINK_CLASS : className;
59
- const normalizedType = normalizeDataType(dataType);
60
-
61
- switch (normalizedType) {
62
- case DATA_TYPES.Address:
63
- return <FormattedAddress value={str as string} className={linkClassName} />;
64
- case DATA_TYPES.Phone:
65
- return <FormattedPhone value={str as string} className={linkClassName} />;
66
- case DATA_TYPES.Url:
67
- return <FormattedUrl value={str as string} className={linkClassName} />;
68
- case DATA_TYPES.Email:
69
- return <FormattedEmail value={str as string} className={linkClassName} />;
70
- default:
71
- return <FormattedText value={value} className={className} />;
72
- }
73
- }
@@ -1,29 +0,0 @@
1
- /** Address as link to Google Maps search. External link: target _blank, rel noopener noreferrer. */
2
-
3
- const GOOGLE_MAPS_SEARCH_BASE = "https://www.google.com/maps/search/?api=1&query=";
4
-
5
- export interface FormattedAddressProps {
6
- /** Full address string (e.g. "10 Main Rd.\nNew York, NY 31349\nUSA"). */
7
- value: string;
8
- className?: string;
9
- }
10
-
11
- export function FormattedAddress({ value, className }: FormattedAddressProps) {
12
- if (!value || !value.trim()) return null;
13
- const url = GOOGLE_MAPS_SEARCH_BASE + encodeURIComponent(value.trim());
14
- return (
15
- <a
16
- href={url}
17
- target="_blank"
18
- rel="noopener noreferrer"
19
- className={className}
20
- aria-label="Open address in Google Maps"
21
- >
22
- {value.split("\n").map((line, i) => (
23
- <span key={i} className="block">
24
- {line}
25
- </span>
26
- ))}
27
- </a>
28
- );
29
- }
@@ -1,17 +0,0 @@
1
- /** Email as mailto: link. */
2
-
3
- export interface FormattedEmailProps {
4
- value: string;
5
- className?: string;
6
- }
7
-
8
- export function FormattedEmail({ value, className }: FormattedEmailProps) {
9
- const str = (value || "").trim();
10
- if (!str) return null;
11
- const href = `mailto:${encodeURIComponent(str)}`;
12
- return (
13
- <a href={href} className={className} aria-label={`Email ${str}`}>
14
- {str}
15
- </a>
16
- );
17
- }
@@ -1,24 +0,0 @@
1
- /** Phone value as tel: link for dialer/VoIP. */
2
-
3
- export interface FormattedPhoneProps {
4
- value: string;
5
- className?: string;
6
- }
7
-
8
- /** Normalizes value for tel: (digits and + only). */
9
- function telHref(value: string): string {
10
- const cleaned = value.replace(/[^\d+]/g, "");
11
- return cleaned ? `tel:${cleaned}` : "#";
12
- }
13
-
14
- export function FormattedPhone({ value, className }: FormattedPhoneProps) {
15
- if (!value) return null;
16
- const trimmed = (value || "").trim();
17
- const href = telHref(trimmed);
18
- if (href === "#") return <span className={className}>{trimmed}</span>;
19
- return (
20
- <a href={href} className={className} aria-label={`Call ${trimmed}`}>
21
- {trimmed}
22
- </a>
23
- );
24
- }
@@ -1,11 +0,0 @@
1
- /** Plain-text field value when dataType has no dedicated formatter. */
2
-
3
- export interface FormattedTextProps {
4
- value: string | number | boolean | null | undefined;
5
- className?: string;
6
- }
7
-
8
- export function FormattedText({ value, className }: FormattedTextProps) {
9
- if (!value) return null;
10
- return <span className={className}>{value as string}</span>;
11
- }