@salesforce/ui-bundle-template-app-react-template-b2e 1.117.2

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 (133) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +52 -0
  3. package/dist/.forceignore +15 -0
  4. package/dist/.husky/pre-commit +4 -0
  5. package/dist/.prettierignore +11 -0
  6. package/dist/.prettierrc +17 -0
  7. package/dist/AGENT.md +193 -0
  8. package/dist/CHANGELOG.md +2128 -0
  9. package/dist/README.md +52 -0
  10. package/dist/config/project-scratch-def.json +13 -0
  11. package/dist/eslint.config.js +7 -0
  12. package/dist/force-app/main/default/uiBundles/reactinternalapp/.forceignore +15 -0
  13. package/dist/force-app/main/default/uiBundles/reactinternalapp/.graphqlrc.yml +2 -0
  14. package/dist/force-app/main/default/uiBundles/reactinternalapp/.prettierignore +9 -0
  15. package/dist/force-app/main/default/uiBundles/reactinternalapp/.prettierrc +11 -0
  16. package/dist/force-app/main/default/uiBundles/reactinternalapp/CHANGELOG.md +10 -0
  17. package/dist/force-app/main/default/uiBundles/reactinternalapp/README.md +35 -0
  18. package/dist/force-app/main/default/uiBundles/reactinternalapp/codegen.yml +95 -0
  19. package/dist/force-app/main/default/uiBundles/reactinternalapp/components.json +18 -0
  20. package/dist/force-app/main/default/uiBundles/reactinternalapp/e2e/app.spec.ts +17 -0
  21. package/dist/force-app/main/default/uiBundles/reactinternalapp/eslint.config.js +169 -0
  22. package/dist/force-app/main/default/uiBundles/reactinternalapp/index.html +12 -0
  23. package/dist/force-app/main/default/uiBundles/reactinternalapp/package.json +69 -0
  24. package/dist/force-app/main/default/uiBundles/reactinternalapp/playwright.config.ts +24 -0
  25. package/dist/force-app/main/default/uiBundles/reactinternalapp/reactinternalapp.uibundle-meta.xml +7 -0
  26. package/dist/force-app/main/default/uiBundles/reactinternalapp/scripts/get-graphql-schema.mjs +68 -0
  27. package/dist/force-app/main/default/uiBundles/reactinternalapp/scripts/rewrite-e2e-assets.mjs +23 -0
  28. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/account/accountSearchService.ts +46 -0
  29. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/account/query/distinctAccountIndustries.graphql +19 -0
  30. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/account/query/distinctAccountTypes.graphql +19 -0
  31. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/account/query/getAccountDetail.graphql +121 -0
  32. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/account/query/searchAccounts.graphql +51 -0
  33. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/graphql-operations-types.ts +11260 -0
  34. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/api/graphqlClient.ts +25 -0
  35. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/app.tsx +17 -0
  36. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/appLayout.tsx +85 -0
  37. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/icons/book.svg +3 -0
  38. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/icons/copy.svg +4 -0
  39. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/icons/rocket.svg +3 -0
  40. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/icons/star.svg +3 -0
  41. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/images/codey-1.png +0 -0
  42. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/images/codey-2.png +0 -0
  43. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/images/codey-3.png +0 -0
  44. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/assets/images/vibe-codey.svg +194 -0
  45. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/AgentforceConversationClient.tsx +168 -0
  46. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/__inherit_AgentforceConversationClient.tsx +3 -0
  47. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/alerts/status-alert.tsx +49 -0
  48. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/layouts/card-layout.tsx +29 -0
  49. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/alert.tsx +76 -0
  50. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/badge.tsx +48 -0
  51. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/breadcrumb.tsx +109 -0
  52. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/button.tsx +67 -0
  53. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/calendar.tsx +232 -0
  54. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/card.tsx +103 -0
  55. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/checkbox.tsx +32 -0
  56. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/collapsible.tsx +33 -0
  57. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/datePicker.tsx +127 -0
  58. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/dialog.tsx +162 -0
  59. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/field.tsx +237 -0
  60. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/index.ts +84 -0
  61. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/input.tsx +19 -0
  62. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/label.tsx +22 -0
  63. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/pagination.tsx +132 -0
  64. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/popover.tsx +89 -0
  65. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/select.tsx +193 -0
  66. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/separator.tsx +26 -0
  67. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/skeleton.tsx +14 -0
  68. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/sonner.tsx +20 -0
  69. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/spinner.tsx +16 -0
  70. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/table.tsx +114 -0
  71. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/components/ui/tabs.tsx +88 -0
  72. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  73. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  74. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  75. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  76. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  77. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  78. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +312 -0
  79. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  80. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/api/objectSearchService.ts +84 -0
  81. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/ActiveFilters.tsx +89 -0
  82. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/FilterContext.tsx +83 -0
  83. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  84. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/PaginationControls.tsx +109 -0
  85. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/SearchBar.tsx +41 -0
  86. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/SortControl.tsx +143 -0
  87. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/BooleanFilter.tsx +78 -0
  88. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/DateFilter.tsx +128 -0
  89. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/DateRangeFilter.tsx +70 -0
  90. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +33 -0
  91. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +97 -0
  92. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +163 -0
  93. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/SearchFilter.tsx +50 -0
  94. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  95. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/components/filters/TextFilter.tsx +91 -0
  96. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/hooks/useAsyncData.ts +54 -0
  97. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/hooks/useCachedAsyncData.ts +184 -0
  98. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/hooks/useDebouncedCallback.ts +34 -0
  99. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/hooks/useObjectSearchParams.ts +252 -0
  100. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/utils/debounce.ts +25 -0
  101. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/utils/fieldUtils.ts +29 -0
  102. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/utils/filterUtils.ts +395 -0
  103. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/features/object-search/utils/sortUtils.ts +38 -0
  104. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/index.ts +6 -0
  105. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/lib/utils.ts +6 -0
  106. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/navigationMenu.tsx +80 -0
  107. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/pages/AccountObjectDetailPage.tsx +361 -0
  108. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/pages/AccountSearch.tsx +305 -0
  109. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/pages/Home.tsx +34 -0
  110. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/pages/NotFound.tsx +18 -0
  111. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/router-utils.tsx +35 -0
  112. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/routes.tsx +32 -0
  113. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/styles/global.css +135 -0
  114. package/dist/force-app/main/default/uiBundles/reactinternalapp/src/types/conversation.ts +33 -0
  115. package/dist/force-app/main/default/uiBundles/reactinternalapp/tsconfig.json +42 -0
  116. package/dist/force-app/main/default/uiBundles/reactinternalapp/tsconfig.node.json +13 -0
  117. package/dist/force-app/main/default/uiBundles/reactinternalapp/ui-bundle.json +7 -0
  118. package/dist/force-app/main/default/uiBundles/reactinternalapp/vite-env.d.ts +1 -0
  119. package/dist/force-app/main/default/uiBundles/reactinternalapp/vite.config.ts +106 -0
  120. package/dist/force-app/main/default/uiBundles/reactinternalapp/vitest-env.d.ts +2 -0
  121. package/dist/force-app/main/default/uiBundles/reactinternalapp/vitest.config.ts +11 -0
  122. package/dist/force-app/main/default/uiBundles/reactinternalapp/vitest.setup.ts +1 -0
  123. package/dist/jest.config.js +6 -0
  124. package/dist/package-lock.json +9995 -0
  125. package/dist/package.json +40 -0
  126. package/dist/scripts/apex/hello.apex +10 -0
  127. package/dist/scripts/graphql-search.sh +191 -0
  128. package/dist/scripts/prepare-import-unique-fields.js +122 -0
  129. package/dist/scripts/setup-cli.mjs +563 -0
  130. package/dist/scripts/sf-project-setup.mjs +66 -0
  131. package/dist/scripts/soql/account.soql +6 -0
  132. package/dist/sfdx-project.json +12 -0
  133. package/package.json +40 -0
@@ -0,0 +1,38 @@
1
+ import { ResultOrder, NullOrder } from "../../../api/graphql-operations-types";
2
+
3
+ export type SortFieldConfig<TFieldName extends string = string> = {
4
+ field: TFieldName;
5
+ label: string;
6
+ };
7
+
8
+ export type SortState<TFieldName extends string = string> = {
9
+ field: TFieldName;
10
+ direction: "ASC" | "DESC";
11
+ };
12
+
13
+ /**
14
+ * Converts a {@link SortState} into a GraphQL order-by object.
15
+ *
16
+ * @typeParam TOrderBy - The GraphQL order-by input type (e.g. `AccountOrderByInput`).
17
+ * @param sort - The current sort state from the UI, or `null` if no sort is applied.
18
+ * @returns An order-by object for the GraphQL query's `orderBy` variable, or
19
+ * `undefined` if no sort is active (which uses the API's default ordering).
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const orderBy = buildOrderBy<AccountOrderByInput>({
24
+ * field: "Name",
25
+ * direction: "ASC",
26
+ * });
27
+ * // orderBy => { Name: { order: ResultOrder.Asc, nulls: NullOrder.Last } }
28
+ * ```
29
+ */
30
+ export function buildOrderBy<TOrderBy>(sort: SortState | null): TOrderBy | undefined {
31
+ if (!sort) return undefined;
32
+ return {
33
+ [sort.field]: {
34
+ order: sort.direction === "ASC" ? ResultOrder.Asc : ResultOrder.Desc,
35
+ nulls: NullOrder.Last,
36
+ },
37
+ } as TOrderBy;
38
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * feature-react-agentforce-conversation-client – ACC Conversation Client
3
+ */
4
+
5
+ export { AgentforceConversationClient } from "./components/AgentforceConversationClient";
6
+ export type { AgentforceConversationClientProps, ResolvedEmbedOptions } from "./types/conversation";
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,80 @@
1
+ import { Link, useLocation } from 'react-router';
2
+ import { getAllRoutes } from './router-utils';
3
+ import { useState } from 'react';
4
+
5
+ export default function NavigationMenu() {
6
+ const [isOpen, setIsOpen] = useState(false);
7
+ const location = useLocation();
8
+
9
+ const isActive = (path: string) => location.pathname === path;
10
+
11
+ const toggleMenu = () => setIsOpen(!isOpen);
12
+
13
+ const navigationRoutes: { path: string; label: string }[] = getAllRoutes()
14
+ .filter(
15
+ route =>
16
+ route.handle?.showInNavigation === true &&
17
+ route.fullPath !== undefined &&
18
+ route.handle?.label !== undefined
19
+ )
20
+ .map(
21
+ route =>
22
+ ({
23
+ path: route.fullPath,
24
+ label: route.handle?.label,
25
+ }) as { path: string; label: string }
26
+ );
27
+
28
+ return (
29
+ <nav className="bg-white border-b border-gray-200">
30
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
31
+ <div className="flex justify-between items-center h-16">
32
+ <Link to="/" className="text-xl font-semibold text-gray-900">
33
+ React App
34
+ </Link>
35
+ <button
36
+ onClick={toggleMenu}
37
+ className="p-2 rounded-md text-gray-700 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
38
+ aria-label="Toggle menu"
39
+ >
40
+ <div className="w-6 h-6 flex flex-col justify-center space-y-1.5">
41
+ <span
42
+ className={`block h-0.5 w-6 bg-current transition-all ${
43
+ isOpen ? 'rotate-45 translate-y-2' : ''
44
+ }`}
45
+ />
46
+ <span
47
+ className={`block h-0.5 w-6 bg-current transition-all ${isOpen ? 'opacity-0' : ''}`}
48
+ />
49
+ <span
50
+ className={`block h-0.5 w-6 bg-current transition-all ${
51
+ isOpen ? '-rotate-45 -translate-y-2' : ''
52
+ }`}
53
+ />
54
+ </div>
55
+ </button>
56
+ </div>
57
+ {isOpen && (
58
+ <div className="pb-4">
59
+ <div className="flex flex-col space-y-2">
60
+ {navigationRoutes.map(item => (
61
+ <Link
62
+ key={item.path}
63
+ to={item.path}
64
+ onClick={() => setIsOpen(false)}
65
+ className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
66
+ isActive(item.path)
67
+ ? 'bg-blue-100 text-blue-700'
68
+ : 'text-gray-700 hover:bg-gray-100'
69
+ }`}
70
+ >
71
+ {item.label}
72
+ </Link>
73
+ ))}
74
+ </div>
75
+ </div>
76
+ )}
77
+ </div>
78
+ </nav>
79
+ );
80
+ }
@@ -0,0 +1,361 @@
1
+ import { useState } from "react";
2
+ import { useParams, useNavigate } from "react-router";
3
+ import { createDataSDK } from "@salesforce/sdk-data";
4
+ import { AlertCircle, ChevronDown, ChevronRight, FileQuestion } from "lucide-react";
5
+ import GET_ACCOUNT_DETAIL from "../api/account/query/getAccountDetail.graphql?raw";
6
+ import type {
7
+ GetAccountDetailQuery,
8
+ GetAccountDetailQueryVariables,
9
+ } from "../api/graphql-operations-types";
10
+ import { Alert, AlertTitle, AlertDescription } from "../components/ui/alert";
11
+ import { Button } from "../components/ui/button";
12
+ import { Card, CardContent } from "../components/ui/card";
13
+ import {
14
+ fieldValue,
15
+ getAddressFieldLines,
16
+ formatDateTimeField,
17
+ } from "../features/object-search/utils/fieldUtils";
18
+ import {
19
+ Collapsible,
20
+ CollapsibleTrigger,
21
+ CollapsibleContent,
22
+ } from "../components/ui/collapsible";
23
+ import { Separator } from "../components/ui/separator";
24
+ import { Skeleton } from "../components/ui/skeleton";
25
+ import { useCachedAsyncData } from "../features/object-search/hooks/useCachedAsyncData";
26
+ import { ObjectBreadcrumb } from "../features/object-search/components/ObjectBreadcrumb";
27
+
28
+ type AccountNode = NonNullable<
29
+ NonNullable<
30
+ NonNullable<NonNullable<GetAccountDetailQuery["uiapi"]["query"]["Account"]>["edges"]>[number]
31
+ >["node"]
32
+ >;
33
+
34
+ async function fetchAccountDetail(recordId: string): Promise<AccountNode | null | undefined> {
35
+ const data = await createDataSDK();
36
+ const response = await data.graphql?.<GetAccountDetailQuery, GetAccountDetailQueryVariables>(
37
+ GET_ACCOUNT_DETAIL,
38
+ { id: recordId },
39
+ );
40
+
41
+ if (response?.errors?.length) {
42
+ throw new Error(response.errors.map((e) => e.message).join("; "));
43
+ }
44
+
45
+ return response?.data?.uiapi?.query?.Account?.edges?.[0]?.node;
46
+ }
47
+
48
+ export default function AccountObjectDetail() {
49
+ const { recordId } = useParams();
50
+ const navigate = useNavigate();
51
+
52
+ const {
53
+ data: account,
54
+ loading,
55
+ error,
56
+ } = useCachedAsyncData(() => fetchAccountDetail(recordId!), [recordId], {
57
+ key: `account:${recordId}`,
58
+ });
59
+
60
+ return (
61
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
62
+ <ObjectBreadcrumb
63
+ listPath="/accounts"
64
+ listLabel="Accounts"
65
+ loading={loading}
66
+ recordName={
67
+ account
68
+ ? (fieldValue(account.Name) ?? "")
69
+ : error
70
+ ? "Error"
71
+ : loading
72
+ ? undefined
73
+ : "Not Found"
74
+ }
75
+ />
76
+
77
+ {/* Loading state */}
78
+ {loading && <AccountDetailSkeleton />}
79
+
80
+ {/* Error state */}
81
+ {error && <AccountDetailError onBack={() => navigate(-1)} />}
82
+
83
+ {/* Not found state */}
84
+ {!loading && !error && !account && <AccountDetailNotFound onBack={() => navigate(-1)} />}
85
+
86
+ {/* Content */}
87
+ {account && <AccountDetailContent account={account} />}
88
+ </div>
89
+ );
90
+ }
91
+
92
+ function AccountDetailContent({ account }: { account: AccountNode }) {
93
+ const billingAddress = getAddressFieldLines({
94
+ street: fieldValue(account.BillingStreet),
95
+ city: fieldValue(account.BillingCity),
96
+ state: fieldValue(account.BillingState),
97
+ postalCode: fieldValue(account.BillingPostalCode),
98
+ country: fieldValue(account.BillingCountry),
99
+ });
100
+
101
+ const shippingAddress = getAddressFieldLines({
102
+ street: fieldValue(account.ShippingStreet),
103
+ city: fieldValue(account.ShippingCity),
104
+ state: fieldValue(account.ShippingState),
105
+ postalCode: fieldValue(account.ShippingPostalCode),
106
+ country: fieldValue(account.ShippingCountry),
107
+ });
108
+
109
+ const dateTimeOptions = { dateStyle: "medium", timeStyle: "short" } as const;
110
+ const createdDate = formatDateTimeField(
111
+ fieldValue(account.CreatedDate),
112
+ undefined,
113
+ dateTimeOptions,
114
+ );
115
+ const lastModifiedDate = formatDateTimeField(
116
+ fieldValue(account.LastModifiedDate),
117
+ undefined,
118
+ dateTimeOptions,
119
+ );
120
+
121
+ return (
122
+ <>
123
+ <h1 className="text-2xl font-bold mb-4">Account: {fieldValue(account.Name)}</h1>
124
+
125
+ <Card>
126
+ <CardContent className="space-y-8 pt-6">
127
+ {/* Top section */}
128
+ <div>
129
+ <div className="space-y-4">
130
+ <FieldRow>
131
+ <FieldItem label="Account Owner">{fieldValue(account.Owner?.Name)}</FieldItem>
132
+ <FieldItem label="Phone">
133
+ <TelephoneField value={fieldValue(account.Phone)} />
134
+ </FieldItem>
135
+ </FieldRow>
136
+ <FieldRow>
137
+ <FieldItem label="Account Name">{fieldValue(account.Name)}</FieldItem>
138
+ <FieldItem label="Fax">
139
+ <TelephoneField value={fieldValue(account.Fax)} />
140
+ </FieldItem>
141
+ </FieldRow>
142
+ <FieldRow>
143
+ <FieldItem label="Parent Account">{fieldValue(account.Parent?.Name)}</FieldItem>
144
+ <FieldItem label="Website">{fieldValue(account.Website)}</FieldItem>
145
+ </FieldRow>
146
+ </div>
147
+ </div>
148
+
149
+ <Separator />
150
+
151
+ {/* Additional Information */}
152
+ <Section title="Additional Information">
153
+ <FieldRow>
154
+ <FieldItem label="Type">{fieldValue(account.Type)}</FieldItem>
155
+ <FieldItem label="Employees">{fieldValue(account.NumberOfEmployees)}</FieldItem>
156
+ </FieldRow>
157
+ <FieldRow>
158
+ <FieldItem label="Industry">{fieldValue(account.Industry)}</FieldItem>
159
+ <FieldItem label="Annual Revenue">{fieldValue(account.AnnualRevenue)}</FieldItem>
160
+ </FieldRow>
161
+ <FieldItem label="Description">{fieldValue(account.Description)}</FieldItem>
162
+ </Section>
163
+
164
+ <Separator />
165
+
166
+ {/* Address Information */}
167
+ <Section title="Address Information">
168
+ <FieldRow>
169
+ <FieldItem label="Billing Address">
170
+ {billingAddress ? billingAddress.map((line, i) => <div key={i}>{line}</div>) : null}
171
+ </FieldItem>
172
+ <FieldItem label="Shipping Address">
173
+ {shippingAddress
174
+ ? shippingAddress.map((line, i) => <div key={i}>{line}</div>)
175
+ : null}
176
+ </FieldItem>
177
+ </FieldRow>
178
+ </Section>
179
+
180
+ <Separator />
181
+
182
+ {/* System Information */}
183
+ <Section title="System Information">
184
+ <FieldRow>
185
+ <FieldItem label="Created By">
186
+ {[fieldValue(account.CreatedBy?.Name), createdDate].filter(Boolean).join(" ") ||
187
+ null}
188
+ </FieldItem>
189
+ <FieldItem label="Last Modified By">
190
+ {[fieldValue(account.LastModifiedBy?.Name), lastModifiedDate]
191
+ .filter(Boolean)
192
+ .join(" ") || null}
193
+ </FieldItem>
194
+ </FieldRow>
195
+ </Section>
196
+ </CardContent>
197
+ </Card>
198
+ </>
199
+ );
200
+ }
201
+
202
+ function TelephoneField({ value }: { value?: string | null }) {
203
+ if (!value) return null;
204
+ return (
205
+ <a href={`tel:${value}`} className="underline">
206
+ {value}
207
+ </a>
208
+ );
209
+ }
210
+
211
+ function FieldItem({ label, children }: { label: string; children: React.ReactNode }) {
212
+ return (
213
+ <div>
214
+ <dt className="text-sm text-muted-foreground">{label}</dt>
215
+ <dd className="mt-0.5">{children ?? "—"}</dd>
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function FieldRow({ children }: { children: React.ReactNode }) {
221
+ return <div className="grid grid-cols-2 gap-x-8 gap-y-4">{children}</div>;
222
+ }
223
+
224
+ function Section({ title, children }: { title: string; children: React.ReactNode }) {
225
+ const [open, setOpen] = useState(true);
226
+ return (
227
+ <Collapsible open={open} onOpenChange={setOpen}>
228
+ <CollapsibleTrigger className="flex items-center gap-2 cursor-pointer text-lg font-semibold py-2">
229
+ {open ? <ChevronDown className="size-5" /> : <ChevronRight className="size-5" />}
230
+ {title}
231
+ </CollapsibleTrigger>
232
+ <CollapsibleContent>
233
+ <div className="mt-2 space-y-4">{children}</div>
234
+ </CollapsibleContent>
235
+ </Collapsible>
236
+ );
237
+ }
238
+
239
+ function AccountDetailError({ onBack }: { onBack: () => void }) {
240
+ return (
241
+ <>
242
+ <Alert variant="destructive" role="alert">
243
+ <AlertCircle />
244
+ <AlertTitle>
245
+ <h2>Failed to load account</h2>
246
+ </AlertTitle>
247
+ <AlertDescription>
248
+ Something went wrong while loading this account. Please try again later.
249
+ </AlertDescription>
250
+ </Alert>
251
+ <div className="mt-4 flex gap-3">
252
+ <Button variant="outline" onClick={onBack}>
253
+ ← Back
254
+ </Button>
255
+ <Button variant="outline" onClick={() => window.location.reload()}>
256
+ Retry
257
+ </Button>
258
+ </div>
259
+ </>
260
+ );
261
+ }
262
+
263
+ function AccountDetailNotFound({ onBack }: { onBack: () => void }) {
264
+ return (
265
+ <Card>
266
+ <CardContent className="flex flex-col items-center justify-center py-16 text-center">
267
+ <FileQuestion className="size-12 text-muted-foreground mb-4" />
268
+ <h2 className="text-lg font-semibold mb-1">Account not found</h2>
269
+ <p className="text-sm text-muted-foreground mb-6">
270
+ The account you're looking for doesn't exist or may have been deleted.
271
+ </p>
272
+ <Button variant="outline" onClick={onBack}>
273
+ ← Go back
274
+ </Button>
275
+ </CardContent>
276
+ </Card>
277
+ );
278
+ }
279
+
280
+ function AccountDetailSkeleton() {
281
+ return (
282
+ <>
283
+ <Skeleton className="h-8 w-56 mb-4" />
284
+
285
+ <Card>
286
+ <CardContent className="space-y-8 pt-6">
287
+ {/* Top section: field rows */}
288
+ <div>
289
+ <div className="space-y-4">
290
+ <SkeletonFieldRow />
291
+ <SkeletonFieldRow />
292
+ <SkeletonFieldRow />
293
+ </div>
294
+ </div>
295
+
296
+ <Separator />
297
+
298
+ {/* Additional Information */}
299
+ <SkeletonSection />
300
+
301
+ <Separator />
302
+
303
+ {/* Address Information */}
304
+ <div className="space-y-4">
305
+ <Skeleton className="h-7 w-48 py-2" />
306
+ <div className="grid grid-cols-2 gap-x-8 gap-y-4">
307
+ <div>
308
+ <Skeleton className="h-4 w-28 mb-1.5" />
309
+ <Skeleton className="h-5 w-44 mb-1" />
310
+ <Skeleton className="h-5 w-36 mb-1" />
311
+ <Skeleton className="h-5 w-28" />
312
+ </div>
313
+ <div>
314
+ <Skeleton className="h-4 w-32 mb-1.5" />
315
+ <Skeleton className="h-5 w-44 mb-1" />
316
+ <Skeleton className="h-5 w-36 mb-1" />
317
+ <Skeleton className="h-5 w-28" />
318
+ </div>
319
+ </div>
320
+ </div>
321
+
322
+ <Separator />
323
+
324
+ {/* System Information */}
325
+ <div className="space-y-4">
326
+ <Skeleton className="h-7 w-48 py-2" />
327
+ <SkeletonFieldRow />
328
+ </div>
329
+ </CardContent>
330
+ </Card>
331
+ </>
332
+ );
333
+ }
334
+
335
+ function SkeletonField() {
336
+ return (
337
+ <div>
338
+ <Skeleton className="h-4 w-24 mb-1.5" />
339
+ <Skeleton className="h-5 w-40" />
340
+ </div>
341
+ );
342
+ }
343
+
344
+ function SkeletonFieldRow() {
345
+ return (
346
+ <div className="grid grid-cols-2 gap-x-8 gap-y-4">
347
+ <SkeletonField />
348
+ <SkeletonField />
349
+ </div>
350
+ );
351
+ }
352
+
353
+ function SkeletonSection() {
354
+ return (
355
+ <div className="space-y-4">
356
+ <Skeleton className="h-7 w-48 py-2" />
357
+ <SkeletonFieldRow />
358
+ <SkeletonFieldRow />
359
+ </div>
360
+ );
361
+ }