@salesforce/webapp-template-app-react-template-b2x-experimental 1.75.1 → 1.76.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.a4drules/skills/webapp-add-react-component/SKILL.md +78 -0
- package/dist/.a4drules/skills/webapp-add-react-component/implementation/component.md +78 -0
- package/dist/.a4drules/skills/webapp-add-react-component/implementation/header-footer.md +124 -0
- package/dist/.a4drules/skills/webapp-add-react-component/implementation/page.md +92 -0
- package/dist/CHANGELOG.md +19 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/package.json +3 -3
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectDetailService.ts +3 -26
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -9
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoService.ts +5 -104
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectSearchData.ts +7 -228
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +1 -20
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/apiUtils.ts +3 -69
- package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/index.ts +125 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webapp-add-react-component
|
|
3
|
+
description: Creates React components, pages, headers, and footers using shadcn UI and Tailwind CSS. Use when the user asks to create a component, add a widget, build a UI element, add a page, create a new page, add a section (e.g. "add a contacts page"), add a header, add a footer, add a navigation bar, or add anything to the web application.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add React Component
|
|
7
|
+
|
|
8
|
+
## Step 1 — Identify the type of component
|
|
9
|
+
|
|
10
|
+
Determine which of these three categories the request falls into, then follow the corresponding section below:
|
|
11
|
+
|
|
12
|
+
- **Page** — user wants a new routed page (e.g. "add a contacts page", "create a dashboard page", "add a settings section")
|
|
13
|
+
- **Header / Footer** — user wants a site-wide header, footer, nav bar, or page footer that appears on every page
|
|
14
|
+
- **Component** — everything else: a widget, card, table, form, dialog, or other UI element placed within an existing page
|
|
15
|
+
|
|
16
|
+
If it is not immediately clear from the user's message, ask:
|
|
17
|
+
|
|
18
|
+
> "Are you looking to add a new page, a site-wide header or footer, or a component within an existing page?"
|
|
19
|
+
|
|
20
|
+
Then follow the matching section.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Clarifying Questions
|
|
25
|
+
|
|
26
|
+
Ask **one question at a time** and wait for the response before asking the next. Stop when you have enough to build accurately — do not guess or assume.
|
|
27
|
+
|
|
28
|
+
### For a Page
|
|
29
|
+
|
|
30
|
+
1. **What is the name and purpose of the page?** (e.g., Contacts, Dashboard, Settings)
|
|
31
|
+
2. **What URL path should it use?** (e.g., `/contacts`, `/dashboard`) — or derive from the page name?
|
|
32
|
+
3. **Should the page appear in the navigation menu?**
|
|
33
|
+
4. **Who can access it?** Public, authenticated users only (`PrivateRoute`), or unauthenticated only (e.g., login — `AuthenticationRoute`)?
|
|
34
|
+
5. **What content or sections should the page include?** (list, form, table, detail view, etc.)
|
|
35
|
+
6. **Does it need to fetch any data?** If so, from where?
|
|
36
|
+
|
|
37
|
+
### For a Header / Footer
|
|
38
|
+
|
|
39
|
+
1. **Header, footer, or both?**
|
|
40
|
+
2. **What should the header contain?** (logo/app name, nav links, user avatar, CTA button, etc.)
|
|
41
|
+
3. **What should the footer contain?** (copyright text, links, social icons, etc.)
|
|
42
|
+
4. **Should the header be sticky (fixed to top while scrolling)?**
|
|
43
|
+
5. **Is there a logo or brand name to display?** (or placeholder?)
|
|
44
|
+
6. **Any specific color scheme or style direction?** (dark background, branded primary color, minimal, etc.)
|
|
45
|
+
7. **Should navigation links appear in the header?** If so, which pages?
|
|
46
|
+
|
|
47
|
+
### For a Component
|
|
48
|
+
|
|
49
|
+
1. **What should the component do?** (display data, accept input, trigger an action, etc.)
|
|
50
|
+
2. **What page or location should it appear on?**
|
|
51
|
+
3. **Is this shared/reusable across pages, or specific to one feature?** (determines file location)
|
|
52
|
+
4. **What data or props does it need?** (static content, props, fetched data)
|
|
53
|
+
5. **Does it need internal state?** (loading, toggle, form state, etc.)
|
|
54
|
+
6. **Are there any specific shadcn components to use?** (Card, Table, Dialog, Form, etc.)
|
|
55
|
+
7. **Should it appear in a specific layout position?** (full-width, sidebar, inline, etc.)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Implementation
|
|
60
|
+
|
|
61
|
+
Once you have identified the type and gathered answers to the clarifying questions, read and follow the corresponding implementation guide:
|
|
62
|
+
|
|
63
|
+
- **Page** — read `implementation/page.md` and follow the instructions there.
|
|
64
|
+
- **Header / Footer** — read `implementation/header-footer.md` and follow the instructions there.
|
|
65
|
+
- **Component** — read `implementation/component.md` and follow the instructions there.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Verification
|
|
70
|
+
|
|
71
|
+
Before completing, run from the web app directory `force-app/main/default/webapplications/<appName>/` (use the actual app folder name):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
cd force-app/main/default/webapplications/<appName> && npm run lint && npm run build
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- **Lint:** MUST result in 0 errors. Fix any ESLint or TypeScript issues.
|
|
78
|
+
- **Build:** MUST succeed. Resolve any compilation or Vite build failures before finishing.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Implementation — Component
|
|
2
|
+
|
|
3
|
+
### Rules
|
|
4
|
+
|
|
5
|
+
1. **Always use shadcn components** from `@/components/ui` — never build raw HTML equivalents for buttons, inputs, cards, alerts, tabs, tables, or labels.
|
|
6
|
+
2. **All styling via Tailwind** — utility classes only. No inline `style={{}}`, CSS Modules, or other styling systems.
|
|
7
|
+
3. **Use design tokens** — prefer `bg-background`, `text-foreground`, `text-muted-foreground`, `border`, `bg-primary`, `text-destructive`, `rounded-lg` over hardcoded colors.
|
|
8
|
+
4. **Use `cn()`** from `@/lib/utils` for conditional or composable class names.
|
|
9
|
+
5. **TypeScript** — functional components with typed props interface; always accept `className?: string`.
|
|
10
|
+
|
|
11
|
+
### File Location — Component
|
|
12
|
+
|
|
13
|
+
| Component type | Location | Export |
|
|
14
|
+
| ---------------------------------------------- | ---------------------------------------- | ---------------------------------------- |
|
|
15
|
+
| Shared UI primitive (reusable across features) | `src/components/ui/` — add to `index.ts` | Named export |
|
|
16
|
+
| Feature-specific (e.g., dashboard widget) | `src/components/<feature>/` | Named export, import directly where used |
|
|
17
|
+
| Page-level layout element | `src/components/layout/` | Named export |
|
|
18
|
+
|
|
19
|
+
### Component Structure
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui";
|
|
23
|
+
import { cn } from "@/lib/utils";
|
|
24
|
+
|
|
25
|
+
interface MyComponentProps {
|
|
26
|
+
title: string;
|
|
27
|
+
value: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function MyComponent({ title, value, className }: MyComponentProps) {
|
|
32
|
+
return (
|
|
33
|
+
<Card className={cn("border", className)}>
|
|
34
|
+
<CardHeader>
|
|
35
|
+
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
|
36
|
+
</CardHeader>
|
|
37
|
+
<CardContent>
|
|
38
|
+
<p className="text-2xl font-semibold text-foreground">{value}</p>
|
|
39
|
+
</CardContent>
|
|
40
|
+
</Card>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### State and Hooks
|
|
46
|
+
|
|
47
|
+
- **Local state only:** keep `useState`, `useReducer`, `useRef` inside the component.
|
|
48
|
+
- **Shared or complex state:** extract to a custom hook in `src/hooks/` (prefix with `use`, e.g. `useFormData`). Do this when more than one component needs the state, or when multiple hooks are composed together.
|
|
49
|
+
|
|
50
|
+
### Adding the Component to a Page
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// In the target page file, e.g. src/pages/HomePage.tsx
|
|
54
|
+
import { MyComponent } from "@/components/<feature>/MyComponent";
|
|
55
|
+
|
|
56
|
+
export default function HomePage() {
|
|
57
|
+
return (
|
|
58
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
59
|
+
<MyComponent title="Status" value="Active" />
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Useful Patterns — Component
|
|
66
|
+
|
|
67
|
+
- **Programmatic navigation:** use `useNavigate` from `react-router`; call `navigate(path)` — consistent with GlobalSearchInput, SearchResultCard, MaintenanceTable, and other components in the web application.
|
|
68
|
+
- **Page container:** `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12`
|
|
69
|
+
- **Icons:** `lucide-react`; add `aria-hidden="true"` on decorative icons
|
|
70
|
+
- **Focus styles:** use `focus-visible:` variants
|
|
71
|
+
- **Multiple visual variants:** use CVA (`cva`) and `VariantProps`
|
|
72
|
+
- **shadcn import barrel:** `import { Button, Card, Input } from "@/components/ui"`
|
|
73
|
+
|
|
74
|
+
### Confirm — Component
|
|
75
|
+
|
|
76
|
+
- Imports use path aliases (`@/`, not deep relative paths)
|
|
77
|
+
- No raw `<button>`, `<input>`, or styled `<div>` where shadcn equivalents exist
|
|
78
|
+
- No inline `style={{}}` — Tailwind only
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Implementation — Header / Footer
|
|
2
|
+
|
|
3
|
+
### Rules
|
|
4
|
+
|
|
5
|
+
1. **Edit `appLayout.tsx` only** — header and footer are layout-level concerns. Never add them to individual page files.
|
|
6
|
+
2. **Never modify `routes.tsx` or `app.tsx`** — the router setup must remain intact.
|
|
7
|
+
3. **Create component files in `src/components/layout/`** — the designated location for layout-level components.
|
|
8
|
+
4. **Use the full-height flex column pattern** — wrap layout in `min-h-screen flex flex-col` so footer stays at bottom.
|
|
9
|
+
5. **Use shadcn and Tailwind** — compose from `@/components/ui`; style with Tailwind utility classes and design tokens.
|
|
10
|
+
6. **Use path aliases** — import with `@/components/layout/...` and `@/components/ui`; no deep relative paths.
|
|
11
|
+
7. **Preserve existing content** — if `appLayout.tsx` already has a `<NavigationMenu />` or other shell elements, keep them in place.
|
|
12
|
+
|
|
13
|
+
### Step 1 — Create the header component (if requested)
|
|
14
|
+
|
|
15
|
+
Create `src/components/layout/AppHeader.tsx`:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { cn } from "@/lib/utils";
|
|
19
|
+
|
|
20
|
+
interface AppHeaderProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function AppHeader({ className }: AppHeaderProps) {
|
|
25
|
+
return (
|
|
26
|
+
<header
|
|
27
|
+
className={cn(
|
|
28
|
+
"w-full border-b bg-background px-4 sm:px-6 lg:px-8 py-4",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
|
33
|
+
<span className="text-lg font-semibold text-foreground">My App</span>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Step 2 — Create the footer component (if requested)
|
|
41
|
+
|
|
42
|
+
Create `src/components/layout/AppFooter.tsx`:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { cn } from "@/lib/utils";
|
|
46
|
+
|
|
47
|
+
interface AppFooterProps {
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function AppFooter({ className }: AppFooterProps) {
|
|
52
|
+
return (
|
|
53
|
+
<footer
|
|
54
|
+
className={cn(
|
|
55
|
+
"w-full border-t bg-background px-4 sm:px-6 lg:px-8 py-4",
|
|
56
|
+
className,
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<div className="max-w-7xl mx-auto text-center text-sm text-muted-foreground">
|
|
60
|
+
© {new Date().getFullYear()} My App. All rights reserved.
|
|
61
|
+
</div>
|
|
62
|
+
</footer>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Step 3 — Edit `appLayout.tsx`
|
|
68
|
+
|
|
69
|
+
Open `src/appLayout.tsx` — this is the **only file to modify** for layout-level additions. Wrap existing content in a flex column and add header above and footer below `<Outlet />`:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { Outlet } from "react-router";
|
|
73
|
+
import { AppHeader } from "@/components/layout/AppHeader";
|
|
74
|
+
import { AppFooter } from "@/components/layout/AppFooter";
|
|
75
|
+
// Keep all existing imports unchanged
|
|
76
|
+
|
|
77
|
+
export default function AppLayout() {
|
|
78
|
+
return (
|
|
79
|
+
<div className="min-h-screen flex flex-col bg-background">
|
|
80
|
+
<AppHeader />
|
|
81
|
+
{/* Keep any existing NavigationMenu or other shell elements here */}
|
|
82
|
+
<main className="flex-1">
|
|
83
|
+
<Outlet />
|
|
84
|
+
</main>
|
|
85
|
+
<AppFooter />
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### File Locations — Header / Footer
|
|
92
|
+
|
|
93
|
+
| Component | File | Export |
|
|
94
|
+
| ------------ | ------------------------------------- | ------------------------------ |
|
|
95
|
+
| Header | `src/components/layout/AppHeader.tsx` | Named export |
|
|
96
|
+
| Footer | `src/components/layout/AppFooter.tsx` | Named export |
|
|
97
|
+
| Layout shell | `src/appLayout.tsx` | Default export (edit in place) |
|
|
98
|
+
|
|
99
|
+
### Why `appLayout.tsx` — Not Pages or Routes
|
|
100
|
+
|
|
101
|
+
`AppLayout` is the single shell rendered at the root route. Every page is a child rendered via `<Outlet />`. Placing the header and footer here ensures they appear on every page without touching individual pages or the route registry.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
AppLayout (appLayout.tsx)
|
|
105
|
+
├── AppHeader ← renders on every page
|
|
106
|
+
├── NavigationMenu ← keep if already present
|
|
107
|
+
├── <Outlet /> ← active page renders here
|
|
108
|
+
└── AppFooter ← renders on every page
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Useful Patterns — Header / Footer
|
|
112
|
+
|
|
113
|
+
- **Sticky header:** add `sticky top-0 z-50` to the `<header>` element
|
|
114
|
+
- **Separator:** use `<Separator />` from `@/components/ui` instead of `border-b`/`border-t` if a visible divider is preferred
|
|
115
|
+
- **Nav links in header:** use `<Button variant="ghost" asChild>` wrapping a React Router `<Link>`
|
|
116
|
+
- **Icons:** `lucide-react`; add `aria-hidden="true"` on decorative icons
|
|
117
|
+
- **Design tokens:** `bg-background`, `text-foreground`, `text-muted-foreground`, `border`, `bg-primary`
|
|
118
|
+
|
|
119
|
+
### Confirm — Header / Footer
|
|
120
|
+
|
|
121
|
+
- Header and footer appear on every page (navigate to at least two routes)
|
|
122
|
+
- Imports use path aliases (`@/components/layout/...`)
|
|
123
|
+
- No inline `style={{}}` — Tailwind only
|
|
124
|
+
- `src/routes.tsx` and `src/app.tsx` are unchanged
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Implementation — Page
|
|
2
|
+
|
|
3
|
+
### Rules
|
|
4
|
+
|
|
5
|
+
1. **`routes.tsx` is the only route registry** — never add routes in `app.tsx` or inside page files.
|
|
6
|
+
2. **All pages are children of the AppLayout route** — do not create top-level routes that bypass the layout shell.
|
|
7
|
+
3. **Default export per page** — each page file has exactly one default-export component.
|
|
8
|
+
4. **Path aliases in all imports** — use `@/pages/...`, `@/components/...`; no deep relative paths.
|
|
9
|
+
5. **No inline styles** — Tailwind utility classes and design tokens only.
|
|
10
|
+
6. **Catch-all last** — `path: '*'` (NotFound) must always remain the last child in the layout route.
|
|
11
|
+
7. **Never modify `appLayout.tsx`** when adding a page — layout changes are a separate concern.
|
|
12
|
+
|
|
13
|
+
### Step 1 — Create the page file
|
|
14
|
+
|
|
15
|
+
Create `src/pages/MyPage.tsx` with a **default export** and the standard page container:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
export default function MyPage() {
|
|
19
|
+
return (
|
|
20
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
21
|
+
<h1 className="text-3xl font-bold text-foreground">My Page</h1>
|
|
22
|
+
<p className="mt-4 text-muted-foreground">Page content goes here.</p>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use shadcn components from `@/components/ui` for UI elements. All styling via Tailwind — no inline `style={{}}`.
|
|
29
|
+
|
|
30
|
+
### Step 2 — Register the route in `routes.tsx`
|
|
31
|
+
|
|
32
|
+
Open `src/routes.tsx`. Import the page and add it inside the layout route's `children` array:
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import MyPage from "@/pages/MyPage";
|
|
36
|
+
|
|
37
|
+
// Inside the layout route's children array (before the catch-all):
|
|
38
|
+
{
|
|
39
|
+
path: "my-page",
|
|
40
|
+
element: <MyPage />,
|
|
41
|
+
handle: { showInNavigation: true, label: "My Page" },
|
|
42
|
+
},
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `path` is a **relative segment** (e.g., `"contacts"`), not an absolute path.
|
|
46
|
+
- Include `handle: { showInNavigation: true, label: "Label" }` only if the page should appear in the navigation menu.
|
|
47
|
+
- The catch-all `path: '*'` must stay **last**.
|
|
48
|
+
|
|
49
|
+
### Step 3 — Apply an auth guard (if needed)
|
|
50
|
+
|
|
51
|
+
| Access type | Guard | Behavior |
|
|
52
|
+
| ---------------------------------- | ----------------------- | --------------------------------------- |
|
|
53
|
+
| Public | None | Direct child of layout |
|
|
54
|
+
| Authenticated only | `<PrivateRoute>` | Redirects to login if not authenticated |
|
|
55
|
+
| Unauthenticated only (e.g., login) | `<AuthenticationRoute>` | Redirects away if already authenticated |
|
|
56
|
+
|
|
57
|
+
Example — private page:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { PrivateRoute } from "@/components/auth/private-route";
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
path: "settings",
|
|
64
|
+
element: <PrivateRoute><SettingsPage /></PrivateRoute>,
|
|
65
|
+
handle: { showInNavigation: true, label: "Settings" },
|
|
66
|
+
},
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use `ROUTES.*` constants from `@/utils/authenticationConfig` for auth-related paths — do not hardcode `/login`, `/profile`, etc.
|
|
70
|
+
|
|
71
|
+
### File Conventions — Page
|
|
72
|
+
|
|
73
|
+
| Concern | Location |
|
|
74
|
+
| ----------------- | ------------------------------------------------------ |
|
|
75
|
+
| Page component | `src/pages/<PageName>.tsx` (default export) |
|
|
76
|
+
| Route definition | `src/routes.tsx` only |
|
|
77
|
+
| Layout shell | `src/appLayout.tsx` — do not modify for page additions |
|
|
78
|
+
| Auth config paths | `ROUTES.*` from `@/utils/authenticationConfig` |
|
|
79
|
+
|
|
80
|
+
### State and Data
|
|
81
|
+
|
|
82
|
+
- **Local state:** `useState`, `useReducer`, `useRef` inside the page component
|
|
83
|
+
- **Shared or complex state:** extract to `src/hooks/` with a `use` prefix (e.g., `useContacts`)
|
|
84
|
+
- **Data fetching:** prefer GraphQL (`executeGraphQL`) or REST utilities in `src/api/`; place shared data logic in `src/hooks/`
|
|
85
|
+
- **Auth context:** `useAuth()` from `@/context/AuthContext` when current user is needed — only valid under `AuthProvider`
|
|
86
|
+
|
|
87
|
+
### Confirm — Page
|
|
88
|
+
|
|
89
|
+
- The page renders inside the app shell (header/nav visible)
|
|
90
|
+
- If `showInNavigation: true`, the link appears in the navigation menu
|
|
91
|
+
- No TypeScript errors; no broken imports; no missing exports
|
|
92
|
+
- Imports use path aliases (`@/`, not deep relative paths)
|
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.76.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.76.0...v1.76.1) (2026-03-06)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.76.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.75.1...v1.76.0) (2026-03-06)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* (template) add skills for base-app ([#208](https://github.com/salesforce-experience-platform-emu/webapps/issues/208)) ([dd5f7c5](https://github.com/salesforce-experience-platform-emu/webapps/commit/dd5f7c59b0d711cade589620c434836f016c5417))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
## [1.75.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.75.0...v1.75.1) (2026-03-05)
|
|
7
26
|
|
|
8
27
|
**Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@salesforce/sdk-data": "^1.
|
|
19
|
-
"@salesforce/webapp-experimental": "^1.
|
|
18
|
+
"@salesforce/sdk-data": "^1.76.1",
|
|
19
|
+
"@salesforce/webapp-experimental": "^1.76.1",
|
|
20
20
|
"@tailwindcss/vite": "^4.1.17",
|
|
21
21
|
"@tanstack/react-form": "^1.28.4",
|
|
22
22
|
"class-variance-authority": "^0.7.1",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
41
41
|
"@graphql-tools/utils": "^11.0.0",
|
|
42
42
|
"@playwright/test": "^1.49.0",
|
|
43
|
-
"@salesforce/vite-plugin-webapp-experimental": "^1.
|
|
43
|
+
"@salesforce/vite-plugin-webapp-experimental": "^1.76.1",
|
|
44
44
|
"@testing-library/jest-dom": "^6.6.3",
|
|
45
45
|
"@testing-library/react": "^16.1.0",
|
|
46
46
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -36,17 +36,9 @@ export function extractFieldsFromLayout(
|
|
|
36
36
|
return optionalFields;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/**
|
|
40
|
-
* Fetches the Full/View layout for an object (REST). Used by detail view to render sections/rows/items.
|
|
41
|
-
*
|
|
42
|
-
* @param objectApiName - Object API name.
|
|
43
|
-
* @param recordTypeId - Record type Id (default master).
|
|
44
|
-
* @param signal - Optional abort signal.
|
|
45
|
-
*/
|
|
46
39
|
export async function getLayout(
|
|
47
40
|
objectApiName: string,
|
|
48
41
|
recordTypeId: string = DEFAULT_RECORD_TYPE_ID,
|
|
49
|
-
signal?: AbortSignal,
|
|
50
42
|
): Promise<LayoutResponse> {
|
|
51
43
|
const params = new URLSearchParams({
|
|
52
44
|
layoutType: "Full",
|
|
@@ -54,14 +46,10 @@ export async function getLayout(
|
|
|
54
46
|
recordTypeId,
|
|
55
47
|
});
|
|
56
48
|
return fetchAndValidate(
|
|
57
|
-
(
|
|
58
|
-
uiApiClient.get(`/layout/${safeEncodePath(objectApiName)}?${params.toString()}`, {
|
|
59
|
-
signal: abortSignal,
|
|
60
|
-
}),
|
|
49
|
+
() => uiApiClient.get(`/layout/${safeEncodePath(objectApiName)}?${params.toString()}`),
|
|
61
50
|
{
|
|
62
51
|
schema: LayoutResponseSchema,
|
|
63
52
|
errorContext: `layout for ${objectApiName}`,
|
|
64
|
-
signal,
|
|
65
53
|
},
|
|
66
54
|
);
|
|
67
55
|
}
|
|
@@ -86,24 +74,13 @@ function optionalFieldsToColumns(optionalFields: string[]): Column[] {
|
|
|
86
74
|
}));
|
|
87
75
|
}
|
|
88
76
|
|
|
89
|
-
/**
|
|
90
|
-
* Fetches everything needed for the detail page: layout (REST), object metadata (GraphQL), single record (GraphQL).
|
|
91
|
-
* Layout drives which fields are requested; getRecordByIdGraphQL fetches that field set by Id.
|
|
92
|
-
*
|
|
93
|
-
* @param objectApiName - Object API name.
|
|
94
|
-
* @param recordId - Record Id.
|
|
95
|
-
* @param recordTypeId - Record type (default master).
|
|
96
|
-
* @param signal - Optional abort signal.
|
|
97
|
-
* @returns { layout, record, objectMetadata } for DetailForm / UiApiDetailForm.
|
|
98
|
-
*/
|
|
99
77
|
export async function getRecordDetail(
|
|
100
78
|
objectApiName: string,
|
|
101
79
|
recordId: string,
|
|
102
80
|
recordTypeId: string = DEFAULT_RECORD_TYPE_ID,
|
|
103
|
-
signal?: AbortSignal,
|
|
104
81
|
): Promise<RecordDetailResult> {
|
|
105
|
-
const layout = await getLayout(objectApiName, recordTypeId
|
|
106
|
-
const objectMetadata = await objectInfoService.getObjectInfoBatch(objectApiName
|
|
82
|
+
const layout = await getLayout(objectApiName, recordTypeId);
|
|
83
|
+
const objectMetadata = await objectInfoService.getObjectInfoBatch(objectApiName);
|
|
107
84
|
const firstResult = objectMetadata?.results?.[0]?.result;
|
|
108
85
|
if (!firstResult) {
|
|
109
86
|
throw new Error(`Object metadata not found for ${objectApiName}`);
|
|
@@ -161,19 +161,10 @@ function buildObjectInfosWithPicklistsQuery(): string {
|
|
|
161
161
|
}`;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/**
|
|
165
|
-
* Fetches object metadata for the given objects via GraphQL.
|
|
166
|
-
*
|
|
167
|
-
* @param apiNames - Object API names (e.g. ["Account", "Contact"]).
|
|
168
|
-
* @param options.objectInfoInputs - When set, picklist values for specified fields are included (API v65.0+).
|
|
169
|
-
* @param options.signal - Optional abort signal.
|
|
170
|
-
* @returns Raw uiapi.objectInfos response (adapt to REST shape via graphQLObjectInfosToBatchResponse).
|
|
171
|
-
*/
|
|
172
164
|
export async function getObjectInfosGraphQL(
|
|
173
165
|
apiNames: string[],
|
|
174
166
|
options?: {
|
|
175
167
|
objectInfoInputs?: ObjectInfoInput[] | null;
|
|
176
|
-
signal?: AbortSignal;
|
|
177
168
|
},
|
|
178
169
|
): Promise<ObjectInfosGraphQLResponse> {
|
|
179
170
|
const names = apiNames.length ? apiNames : [];
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import { uiApiClient } from "@salesforce/webapp-experimental/api";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import type { SearchResultsResponse, KeywordSearchResult } from "../types/search/searchResults";
|
|
4
|
-
import { SearchResultsResponseSchema } from "../types/search/searchResults";
|
|
5
|
-
import { FilterCriteriaArraySchema } from "../types/filters/filters";
|
|
6
2
|
import type { Filter } from "../types/filters/filters";
|
|
7
3
|
import { FilterArraySchema } from "../types/filters/filters";
|
|
8
4
|
import type { PicklistValue } from "../types/filters/picklist";
|
|
@@ -18,7 +14,7 @@ import {
|
|
|
18
14
|
* Object info and search service.
|
|
19
15
|
*
|
|
20
16
|
* - getObjectInfoBatch / getPicklistValues: GraphQL (objectInfoGraphQLService).
|
|
21
|
-
* - getObjectListFilters
|
|
17
|
+
* - getObjectListFilters: REST (search-info).
|
|
22
18
|
* Hooks use this service; components do not call it directly.
|
|
23
19
|
*
|
|
24
20
|
* @module api/objectInfoService
|
|
@@ -36,18 +32,7 @@ function getObjectInfoBatchCacheKey(objectApiNames: string): string {
|
|
|
36
32
|
const objectInfoBatchCache = new Map<string, ObjectInfoBatchResponse>();
|
|
37
33
|
const objectInfoBatchInFlight = new Map<string, Promise<ObjectInfoBatchResponse>>();
|
|
38
34
|
|
|
39
|
-
|
|
40
|
-
* Fetches batch object information for the specified objects via GraphQL (uiapi.objectInfos).
|
|
41
|
-
* Results are cached by object set so List, Home, and Detail views share one request.
|
|
42
|
-
*
|
|
43
|
-
* @param objectApiNames - Comma-separated list of object API names (e.g., "Account,AccountBrand")
|
|
44
|
-
* @param signal - Optional AbortSignal to cancel the request
|
|
45
|
-
* @returns Promise resolving to the object info batch response (REST-compatible shape)
|
|
46
|
-
*/
|
|
47
|
-
export async function getObjectInfoBatch(
|
|
48
|
-
objectApiNames: string,
|
|
49
|
-
signal?: AbortSignal,
|
|
50
|
-
): Promise<ObjectInfoBatchResponse> {
|
|
35
|
+
export async function getObjectInfoBatch(objectApiNames: string): Promise<ObjectInfoBatchResponse> {
|
|
51
36
|
const names = objectApiNames
|
|
52
37
|
.split(",")
|
|
53
38
|
.map((s) => s.trim())
|
|
@@ -62,7 +47,7 @@ export async function getObjectInfoBatch(
|
|
|
62
47
|
if (inFlight) return inFlight;
|
|
63
48
|
const promise = (async () => {
|
|
64
49
|
try {
|
|
65
|
-
const response = await getObjectInfosGraphQL(names
|
|
50
|
+
const response = await getObjectInfosGraphQL(names);
|
|
66
51
|
const nodes = response?.uiapi?.objectInfos ?? [];
|
|
67
52
|
const result = graphQLObjectInfosToBatchResponse(nodes, names);
|
|
68
53
|
objectInfoBatchCache.set(key, result);
|
|
@@ -75,24 +60,9 @@ export async function getObjectInfoBatch(
|
|
|
75
60
|
return promise;
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
|
|
79
|
-
* Fetches list filters for a specific object.
|
|
80
|
-
* Salesforce Search supports "Search Filters" (refinements) which are configured per object.
|
|
81
|
-
* This API returns the available filters (e.g., "Close Date", "Stage") that the user
|
|
82
|
-
* can use to narrow down the search results.
|
|
83
|
-
* @param objectApiName - The API name of the object (e.g., "Account")
|
|
84
|
-
* @param signal - Optional AbortSignal to cancel the request
|
|
85
|
-
* @returns Promise resolving to the search filters array
|
|
86
|
-
*/
|
|
87
|
-
export async function getObjectListFilters(
|
|
88
|
-
objectApiName: string,
|
|
89
|
-
signal?: AbortSignal,
|
|
90
|
-
): Promise<Filter[]> {
|
|
63
|
+
export async function getObjectListFilters(objectApiName: string): Promise<Filter[]> {
|
|
91
64
|
return fetchAndValidate(
|
|
92
|
-
(
|
|
93
|
-
uiApiClient.get(`/search-info/${safeEncodePath(objectApiName)}/filters`, {
|
|
94
|
-
signal: abortSignal,
|
|
95
|
-
}),
|
|
65
|
+
() => uiApiClient.get(`/search-info/${safeEncodePath(objectApiName)}/filters`),
|
|
96
66
|
{
|
|
97
67
|
schema: FilterArraySchema,
|
|
98
68
|
errorContext: `filters for ${objectApiName}`,
|
|
@@ -100,25 +70,14 @@ export async function getObjectListFilters(
|
|
|
100
70
|
if (!data) return [];
|
|
101
71
|
return Array.isArray(data) ? data : (data as { filters?: unknown }).filters || [];
|
|
102
72
|
},
|
|
103
|
-
signal,
|
|
104
73
|
},
|
|
105
74
|
);
|
|
106
75
|
}
|
|
107
76
|
|
|
108
|
-
/**
|
|
109
|
-
* Fetches picklist values for a specific field via GraphQL (uiapi.objectInfos with objectInfoInputs).
|
|
110
|
-
*
|
|
111
|
-
* @param objectApiName - The API name of the object (e.g., "Account")
|
|
112
|
-
* @param fieldName - The API name of the field (e.g., "Type")
|
|
113
|
-
* @param recordTypeId - Optional record type ID (defaults to "012000000000000AAA" which is the default/master record type)
|
|
114
|
-
* @param signal - Optional AbortSignal to cancel the request
|
|
115
|
-
* @returns Promise resolving to an array of picklist values
|
|
116
|
-
*/
|
|
117
77
|
export async function getPicklistValues(
|
|
118
78
|
objectApiName: string,
|
|
119
79
|
fieldName: string,
|
|
120
80
|
recordTypeId: string = "012000000000000AAA",
|
|
121
|
-
signal?: AbortSignal,
|
|
122
81
|
): Promise<PicklistValue[]> {
|
|
123
82
|
const response = await getObjectInfosGraphQL([objectApiName], {
|
|
124
83
|
objectInfoInputs: [
|
|
@@ -127,7 +86,6 @@ export async function getPicklistValues(
|
|
|
127
86
|
fieldNames: [fieldName],
|
|
128
87
|
},
|
|
129
88
|
],
|
|
130
|
-
signal,
|
|
131
89
|
});
|
|
132
90
|
const nodes = response?.uiapi?.objectInfos ?? [];
|
|
133
91
|
const node = nodes[0];
|
|
@@ -135,65 +93,8 @@ export async function getPicklistValues(
|
|
|
135
93
|
return extractPicklistValuesFromGraphQLObjectInfo(node, fieldName, recordTypeId);
|
|
136
94
|
}
|
|
137
95
|
|
|
138
|
-
// Zod Schema for Search Parameters
|
|
139
|
-
const SearchParamsSchema = z.object({
|
|
140
|
-
filters: FilterCriteriaArraySchema.optional(),
|
|
141
|
-
pageSize: z.number().optional(),
|
|
142
|
-
pageToken: z.string().optional(),
|
|
143
|
-
sortBy: z.string().optional(),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Search parameters for keyword search
|
|
148
|
-
*/
|
|
149
|
-
export type SearchParams = z.infer<typeof SearchParamsSchema>;
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Performs a keyword search on a specific object.
|
|
153
|
-
* Returns records that match the text query along with pagination information.
|
|
154
|
-
*
|
|
155
|
-
* @param query - The search query string
|
|
156
|
-
* @param objectApiName - The API name of the object to search (e.g., "Account")
|
|
157
|
-
* @param params - Optional search parameters (pageSize, pageToken, filters, sortBy)
|
|
158
|
-
* @param signal - Optional AbortSignal to cancel the request
|
|
159
|
-
* @returns Promise resolving to the keyword search result with records and pagination tokens
|
|
160
|
-
*/
|
|
161
|
-
export async function searchResults(
|
|
162
|
-
query: string,
|
|
163
|
-
objectApiName: string,
|
|
164
|
-
params?: SearchParams,
|
|
165
|
-
signal?: AbortSignal,
|
|
166
|
-
): Promise<KeywordSearchResult> {
|
|
167
|
-
const searchParams = new URLSearchParams({
|
|
168
|
-
q: query,
|
|
169
|
-
objectApiName: objectApiName,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const body = {
|
|
173
|
-
filters: params?.filters ?? [],
|
|
174
|
-
pageSize: params?.pageSize ?? 50,
|
|
175
|
-
pageToken: params?.pageToken ?? "0",
|
|
176
|
-
sortBy: params?.sortBy ?? "",
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const response = await fetchAndValidate<SearchResultsResponse>(
|
|
180
|
-
(abortSignal) =>
|
|
181
|
-
uiApiClient.post(`/search/results/keyword?${searchParams.toString()}`, body, {
|
|
182
|
-
signal: abortSignal,
|
|
183
|
-
}),
|
|
184
|
-
{
|
|
185
|
-
schema: SearchResultsResponseSchema,
|
|
186
|
-
errorContext: `search results for ${objectApiName} with query "${query}"`,
|
|
187
|
-
signal,
|
|
188
|
-
},
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
return response.keywordSearchResult;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
96
|
export const objectInfoService = {
|
|
195
97
|
getObjectInfoBatch,
|
|
196
98
|
getObjectListFilters,
|
|
197
99
|
getPicklistValues,
|
|
198
|
-
searchResults,
|
|
199
100
|
};
|
|
@@ -6,26 +6,16 @@
|
|
|
6
6
|
* - getSharedFilters: module-level deduplication for getObjectListFilters across hook instances.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useState, useEffect
|
|
10
|
-
import { objectInfoService
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
SearchResultRecord,
|
|
14
|
-
SearchResultRecordData,
|
|
15
|
-
} from "../types/search/searchResults";
|
|
16
|
-
import type { Filter, FilterCriteria } from "../types/filters/filters";
|
|
9
|
+
import { useState, useEffect } from "react";
|
|
10
|
+
import { objectInfoService } from "../api/objectInfoService";
|
|
11
|
+
import type { Column } from "../types/search/searchResults";
|
|
12
|
+
import type { Filter } from "../types/filters/filters";
|
|
17
13
|
import type { PicklistValue } from "../types/filters/picklist";
|
|
18
|
-
import { createFiltersKey } from "../utils/cacheUtils";
|
|
19
14
|
|
|
20
15
|
// --- Shared filters cache (deduplicates getObjectListFilters across useObjectColumns + useObjectFilters) ---
|
|
21
16
|
const sharedFiltersCache = new Map<string, Filter[]>();
|
|
22
17
|
const sharedFiltersInFlight = new Map<string, Promise<Filter[]>>();
|
|
23
18
|
|
|
24
|
-
/**
|
|
25
|
-
* Returns filters for the object, deduplicating the API call across hook instances.
|
|
26
|
-
* Does not pass abort signal to the API so the shared request is not aborted when
|
|
27
|
-
* one consumer's effect cleans up (e.g. React Strict Mode); callers still guard with isCancelled.
|
|
28
|
-
*/
|
|
29
19
|
function getSharedFilters(objectApiName: string): Promise<Filter[]> {
|
|
30
20
|
const cached = sharedFiltersCache.get(objectApiName);
|
|
31
21
|
if (cached) return Promise.resolve(cached);
|
|
@@ -100,7 +90,6 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
|
|
|
100
90
|
}
|
|
101
91
|
|
|
102
92
|
let isCancelled = false;
|
|
103
|
-
const ac = new AbortController();
|
|
104
93
|
|
|
105
94
|
const run = async () => {
|
|
106
95
|
setState((s) => ({ ...s, loading: true, error: null }));
|
|
@@ -111,12 +100,9 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
|
|
|
111
100
|
const selectFilters = filters.filter((f) => f.affordance?.toLowerCase() === "select");
|
|
112
101
|
const picklistPromises = selectFilters.map((f) =>
|
|
113
102
|
objectInfoService
|
|
114
|
-
.getPicklistValues(objectApiName!, f.targetFieldPath
|
|
103
|
+
.getPicklistValues(objectApiName!, f.targetFieldPath)
|
|
115
104
|
.then((values) => ({ fieldPath: f.targetFieldPath, values }))
|
|
116
|
-
.catch((
|
|
117
|
-
if (err?.name === "AbortError") throw err;
|
|
118
|
-
return { fieldPath: f.targetFieldPath, values: [] as PicklistValue[] };
|
|
119
|
-
}),
|
|
105
|
+
.catch(() => ({ fieldPath: f.targetFieldPath, values: [] as PicklistValue[] })),
|
|
120
106
|
);
|
|
121
107
|
const picklistResults = await Promise.all(picklistPromises);
|
|
122
108
|
if (isCancelled) return;
|
|
@@ -134,7 +120,7 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
|
|
|
134
120
|
error: null,
|
|
135
121
|
});
|
|
136
122
|
} catch (err) {
|
|
137
|
-
if (isCancelled
|
|
123
|
+
if (isCancelled) return;
|
|
138
124
|
setState((s) => ({
|
|
139
125
|
...s,
|
|
140
126
|
columns: [],
|
|
@@ -149,7 +135,6 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
|
|
|
149
135
|
run();
|
|
150
136
|
return () => {
|
|
151
137
|
isCancelled = true;
|
|
152
|
-
ac.abort();
|
|
153
138
|
};
|
|
154
139
|
}, [objectApiName]);
|
|
155
140
|
|
|
@@ -169,212 +154,6 @@ export function useObjectColumns(objectApiName: string | null) {
|
|
|
169
154
|
};
|
|
170
155
|
}
|
|
171
156
|
|
|
172
|
-
/**
|
|
173
|
-
* Hook: useObjectSearchResults
|
|
174
|
-
*
|
|
175
|
-
* Fetches search results for a specific object based on the provided query parameters.
|
|
176
|
-
* Maintains the *latest* result set for the object in state to prevent redundant
|
|
177
|
-
* network requests when the component re-renders with the same parameters.
|
|
178
|
-
* Includes debouncing for search queries (but not pagination).
|
|
179
|
-
*
|
|
180
|
-
* @param objectApiName - The API name of the object to search
|
|
181
|
-
* @param searchQuery - The search query string
|
|
182
|
-
* @param searchPageSize - Number of results per page (default: 50)
|
|
183
|
-
* @param searchPageToken - Pagination token (default: '0')
|
|
184
|
-
* @param filters - Array of filter criteria to apply (default: [])
|
|
185
|
-
* @param sortBy - Sort field and direction (default: 'relevance')
|
|
186
|
-
* @returns Object containing results array, pagination tokens, loading state, and error state
|
|
187
|
-
*
|
|
188
|
-
* @example
|
|
189
|
-
* ```tsx
|
|
190
|
-
* const { results, nextPageToken, previousPageToken, currentPageToken, resultsLoading, resultsError } = useObjectSearchResults(
|
|
191
|
-
* 'Account',
|
|
192
|
-
* 'test query',
|
|
193
|
-
* 25,
|
|
194
|
-
* '0',
|
|
195
|
-
* [{ objectApiName: 'Account', fieldPath: 'Name', operator: 'contains', values: ['test'] }]
|
|
196
|
-
* );
|
|
197
|
-
* ```
|
|
198
|
-
*/
|
|
199
|
-
export function useObjectSearchResults(
|
|
200
|
-
objectApiName: string | null,
|
|
201
|
-
searchQuery: string,
|
|
202
|
-
searchPageSize: number = 50,
|
|
203
|
-
searchPageToken: string = "0",
|
|
204
|
-
filters: FilterCriteria[] = [],
|
|
205
|
-
sortBy: string = "relevance",
|
|
206
|
-
) {
|
|
207
|
-
const [resultsCache, setResultsCache] = useState<
|
|
208
|
-
Record<
|
|
209
|
-
string,
|
|
210
|
-
{
|
|
211
|
-
results: SearchResultRecord[];
|
|
212
|
-
query: string;
|
|
213
|
-
pageToken: string;
|
|
214
|
-
pageSize: number;
|
|
215
|
-
filtersKey: string;
|
|
216
|
-
sortBy: string;
|
|
217
|
-
nextPageToken: string | null;
|
|
218
|
-
previousPageToken: string | null;
|
|
219
|
-
currentPageToken: string;
|
|
220
|
-
}
|
|
221
|
-
>
|
|
222
|
-
>({});
|
|
223
|
-
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
|
224
|
-
const [error, setError] = useState<Record<string, string | null>>({});
|
|
225
|
-
|
|
226
|
-
const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
227
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
228
|
-
const resultsCacheRef = useRef(resultsCache);
|
|
229
|
-
|
|
230
|
-
const filtersKey = useMemo(() => {
|
|
231
|
-
const filtersArray = Array.isArray(filters) ? filters : [];
|
|
232
|
-
return createFiltersKey(filtersArray);
|
|
233
|
-
}, [filters]);
|
|
234
|
-
|
|
235
|
-
useEffect(() => {
|
|
236
|
-
resultsCacheRef.current = resultsCache;
|
|
237
|
-
}, [resultsCache]);
|
|
238
|
-
|
|
239
|
-
useEffect(() => {
|
|
240
|
-
if (!objectApiName || !searchQuery.trim()) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
let isCancelled = false;
|
|
245
|
-
const abortController = new AbortController();
|
|
246
|
-
|
|
247
|
-
if (abortControllerRef.current) {
|
|
248
|
-
abortControllerRef.current.abort();
|
|
249
|
-
}
|
|
250
|
-
abortControllerRef.current = abortController;
|
|
251
|
-
|
|
252
|
-
if (debounceTimeout.current) {
|
|
253
|
-
clearTimeout(debounceTimeout.current);
|
|
254
|
-
debounceTimeout.current = null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const cached = resultsCacheRef.current[objectApiName];
|
|
258
|
-
if (
|
|
259
|
-
!abortController.signal.aborted &&
|
|
260
|
-
cached &&
|
|
261
|
-
cached.query === searchQuery &&
|
|
262
|
-
cached.pageToken === searchPageToken &&
|
|
263
|
-
cached.pageSize === searchPageSize &&
|
|
264
|
-
cached.filtersKey === filtersKey &&
|
|
265
|
-
cached.sortBy === sortBy
|
|
266
|
-
) {
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (abortController.signal.aborted) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const fetchResults = async () => {
|
|
275
|
-
setLoading((prev) => ({ ...prev, [objectApiName]: true }));
|
|
276
|
-
setError((prev) => ({ ...prev, [objectApiName]: null }));
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
const searchParams: SearchParams = {
|
|
280
|
-
sortBy: sortBy === "relevance" ? "" : sortBy,
|
|
281
|
-
filters: filters,
|
|
282
|
-
pageSize: searchPageSize,
|
|
283
|
-
pageToken: searchPageToken,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
const keywordSearchResult = await objectInfoService.searchResults(
|
|
287
|
-
searchQuery,
|
|
288
|
-
objectApiName,
|
|
289
|
-
searchParams,
|
|
290
|
-
abortController.signal,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
if (isCancelled || abortController.signal.aborted) return;
|
|
294
|
-
|
|
295
|
-
const normalizedRecords = keywordSearchResult.records.map((r) => ({
|
|
296
|
-
record: r.record as SearchResultRecordData,
|
|
297
|
-
highlightInfo: r.highlightInfo,
|
|
298
|
-
searchInfo: r.searchInfo,
|
|
299
|
-
}));
|
|
300
|
-
|
|
301
|
-
const nextPageToken: string | null = keywordSearchResult.nextPageToken ?? null;
|
|
302
|
-
const previousPageToken: string | null = keywordSearchResult.previousPageToken ?? null;
|
|
303
|
-
|
|
304
|
-
setResultsCache((prev): typeof prev => ({
|
|
305
|
-
...prev,
|
|
306
|
-
[objectApiName]: {
|
|
307
|
-
results: normalizedRecords,
|
|
308
|
-
query: searchQuery,
|
|
309
|
-
pageToken: searchPageToken,
|
|
310
|
-
pageSize: searchPageSize,
|
|
311
|
-
filtersKey: filtersKey,
|
|
312
|
-
sortBy,
|
|
313
|
-
nextPageToken,
|
|
314
|
-
previousPageToken,
|
|
315
|
-
currentPageToken: keywordSearchResult.currentPageToken,
|
|
316
|
-
},
|
|
317
|
-
}));
|
|
318
|
-
} catch (err) {
|
|
319
|
-
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
setError((prev) => ({ ...prev, [objectApiName]: "Unable to load search results" }));
|
|
323
|
-
// Cache empty result so we skip refetch on remount (avoid infinite loop on API error)
|
|
324
|
-
setResultsCache((prev) => ({
|
|
325
|
-
...prev,
|
|
326
|
-
[objectApiName]: {
|
|
327
|
-
results: [],
|
|
328
|
-
query: searchQuery,
|
|
329
|
-
pageToken: searchPageToken,
|
|
330
|
-
pageSize: searchPageSize,
|
|
331
|
-
filtersKey: filtersKey,
|
|
332
|
-
sortBy,
|
|
333
|
-
nextPageToken: null,
|
|
334
|
-
previousPageToken: null,
|
|
335
|
-
currentPageToken: searchPageToken,
|
|
336
|
-
},
|
|
337
|
-
}));
|
|
338
|
-
} finally {
|
|
339
|
-
if (!isCancelled) {
|
|
340
|
-
setLoading((prev) => ({ ...prev, [objectApiName]: false }));
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
if (searchPageToken === "0") {
|
|
346
|
-
debounceTimeout.current = setTimeout(() => {
|
|
347
|
-
fetchResults();
|
|
348
|
-
}, 300);
|
|
349
|
-
} else {
|
|
350
|
-
fetchResults();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return () => {
|
|
354
|
-
isCancelled = true;
|
|
355
|
-
abortController.abort();
|
|
356
|
-
if (debounceTimeout.current) {
|
|
357
|
-
clearTimeout(debounceTimeout.current);
|
|
358
|
-
debounceTimeout.current = null;
|
|
359
|
-
}
|
|
360
|
-
if (abortControllerRef.current === abortController) {
|
|
361
|
-
abortControllerRef.current = null;
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
}, [objectApiName, searchQuery, searchPageSize, searchPageToken, filtersKey, sortBy]);
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
results: objectApiName ? resultsCache[objectApiName]?.results || [] : [],
|
|
368
|
-
nextPageToken: objectApiName ? resultsCache[objectApiName]?.nextPageToken || null : null,
|
|
369
|
-
previousPageToken: objectApiName
|
|
370
|
-
? resultsCache[objectApiName]?.previousPageToken || null
|
|
371
|
-
: null,
|
|
372
|
-
currentPageToken: objectApiName ? resultsCache[objectApiName]?.currentPageToken || "0" : "0",
|
|
373
|
-
resultsLoading: objectApiName ? loading[objectApiName] || false : false,
|
|
374
|
-
resultsError: objectApiName ? error[objectApiName] || null : null,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
157
|
/**
|
|
379
158
|
* Hook: useObjectFilters
|
|
380
159
|
* Thin wrapper over useObjectListMetadata for backward compatibility.
|
|
@@ -16,7 +16,6 @@ export interface UseRecordDetailLayoutParams {
|
|
|
16
16
|
objectApiName: string | null;
|
|
17
17
|
recordId: string | null;
|
|
18
18
|
recordTypeId?: string | null;
|
|
19
|
-
/** When provided, skips the fetch and uses this data (avoids duplicate API calls when parent already fetched). Callers should memoize this (e.g. useMemo) to avoid unnecessary effect runs. */
|
|
20
19
|
initialData?: {
|
|
21
20
|
layout: LayoutResponse;
|
|
22
21
|
record: GraphQLRecordNode;
|
|
@@ -25,7 +24,6 @@ export interface UseRecordDetailLayoutParams {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
const MAX_CACHE_SIZE = 50;
|
|
28
|
-
/** Cache entries older than this are treated as stale and refetched. */
|
|
29
27
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
30
28
|
|
|
31
29
|
type CacheEntry = {
|
|
@@ -35,17 +33,6 @@ type CacheEntry = {
|
|
|
35
33
|
cachedAt: number;
|
|
36
34
|
};
|
|
37
35
|
|
|
38
|
-
/**
|
|
39
|
-
* Detail page data: layout (REST), object metadata (GraphQL), single record (GraphQL).
|
|
40
|
-
*
|
|
41
|
-
* Calls objectDetailService.getRecordDetail once per objectApiName/recordId/recordTypeId.
|
|
42
|
-
* Caches result in memory (TTL 5min, max 50 entries). Used by DetailPage and UiApiDetailForm.
|
|
43
|
-
*
|
|
44
|
-
* @param objectApiName - Object API name.
|
|
45
|
-
* @param recordId - Record Id.
|
|
46
|
-
* @param recordTypeId - Optional record type (default master).
|
|
47
|
-
* @returns { layout, record, objectMetadata, loading, error }.
|
|
48
|
-
*/
|
|
49
36
|
export function useRecordDetailLayout({
|
|
50
37
|
objectApiName,
|
|
51
38
|
recordId,
|
|
@@ -71,7 +58,6 @@ export function useRecordDetailLayout({
|
|
|
71
58
|
return;
|
|
72
59
|
}
|
|
73
60
|
|
|
74
|
-
// Skip fetch when parent already provided data (avoids duplicate API calls)
|
|
75
61
|
if (
|
|
76
62
|
initialData?.layout != null &&
|
|
77
63
|
initialData?.record != null &&
|
|
@@ -92,7 +78,6 @@ export function useRecordDetailLayout({
|
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
let isCancelled = false;
|
|
95
|
-
const abortController = new AbortController();
|
|
96
81
|
|
|
97
82
|
const fetchDetail = async () => {
|
|
98
83
|
setLoading(true);
|
|
@@ -107,7 +92,6 @@ export function useRecordDetailLayout({
|
|
|
107
92
|
objectApiName,
|
|
108
93
|
recordId,
|
|
109
94
|
recordTypeId ?? undefined,
|
|
110
|
-
abortController.signal,
|
|
111
95
|
);
|
|
112
96
|
|
|
113
97
|
if (isCancelled) return;
|
|
@@ -127,9 +111,7 @@ export function useRecordDetailLayout({
|
|
|
127
111
|
setRecord(recordData);
|
|
128
112
|
setObjectMetadata(objectMetadataData);
|
|
129
113
|
} catch (err) {
|
|
130
|
-
if (isCancelled
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
114
|
+
if (isCancelled) return;
|
|
133
115
|
setError("Failed to load record details");
|
|
134
116
|
} finally {
|
|
135
117
|
if (!isCancelled) {
|
|
@@ -142,7 +124,6 @@ export function useRecordDetailLayout({
|
|
|
142
124
|
|
|
143
125
|
return () => {
|
|
144
126
|
isCancelled = true;
|
|
145
|
-
abortController.abort();
|
|
146
127
|
};
|
|
147
128
|
}, [objectApiName, recordId, recordTypeId, cacheKey, initialData]);
|
|
148
129
|
|
|
@@ -2,77 +2,31 @@
|
|
|
2
2
|
* API Utilities
|
|
3
3
|
*
|
|
4
4
|
* Generic utility functions for API requests, validation, and URL handling.
|
|
5
|
-
* These utilities are framework-agnostic and can be reused across different API services.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
import type { ZodSchema } from "zod";
|
|
9
8
|
|
|
10
|
-
/**
|
|
11
|
-
* Options for fetchAndValidate utility function
|
|
12
|
-
*/
|
|
13
9
|
export interface FetchAndValidateOptions<T> {
|
|
14
|
-
/** Zod schema for validation */
|
|
15
10
|
schema: ZodSchema<T>;
|
|
16
|
-
/** Error context for better error messages (e.g., "object info batch", "list info") */
|
|
17
11
|
errorContext: string;
|
|
18
|
-
/** Optional function to extract/transform data from response before validation */
|
|
19
12
|
extractData?: (data: unknown) => unknown;
|
|
20
|
-
/** Optional AbortSignal to cancel the request */
|
|
21
|
-
signal?: AbortSignal;
|
|
22
13
|
}
|
|
23
14
|
|
|
24
|
-
/**
|
|
25
|
-
* Generic utility function to fetch, parse, and validate API responses.
|
|
26
|
-
* Handles common patterns: fetch -> check status -> parse JSON -> validate Zod -> handle errors
|
|
27
|
-
*
|
|
28
|
-
* @param fetchFn - Function that returns a Promise<Response> (e.g., uiApiClient.get or uiApiClient.post)
|
|
29
|
-
* @param options - Configuration options including schema, error context, optional data extraction, and AbortSignal
|
|
30
|
-
* @returns Promise resolving to validated data of type T
|
|
31
|
-
*
|
|
32
|
-
* @remarks
|
|
33
|
-
* - Handles abort signals properly to prevent race conditions
|
|
34
|
-
* - Provides detailed error messages with context
|
|
35
|
-
* - Validates responses using Zod schemas for type safety
|
|
36
|
-
* - Supports data extraction/transformation before validation
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```tsx
|
|
40
|
-
* const data = await fetchAndValidate(
|
|
41
|
-
* (signal) => apiClient.get('/endpoint', { signal }),
|
|
42
|
-
* {
|
|
43
|
-
* schema: MySchema,
|
|
44
|
-
* errorContext: 'user data',
|
|
45
|
-
* extractData: (data) => data.items,
|
|
46
|
-
* signal: abortController.signal
|
|
47
|
-
* }
|
|
48
|
-
* );
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
15
|
export async function fetchAndValidate<T>(
|
|
52
|
-
fetchFn: (
|
|
16
|
+
fetchFn: () => Promise<Response>,
|
|
53
17
|
options: FetchAndValidateOptions<T>,
|
|
54
18
|
): Promise<T> {
|
|
55
|
-
const { schema, errorContext, extractData
|
|
19
|
+
const { schema, errorContext, extractData } = options;
|
|
56
20
|
|
|
57
21
|
try {
|
|
58
|
-
const response = await fetchFn(
|
|
59
|
-
|
|
60
|
-
if (signal?.aborted) {
|
|
61
|
-
throw new DOMException("The operation was aborted.", "AbortError");
|
|
62
|
-
}
|
|
22
|
+
const response = await fetchFn();
|
|
63
23
|
|
|
64
24
|
if (!response.ok) {
|
|
65
25
|
throw new Error(`Failed to fetch ${errorContext}: ${response.status} ${response.statusText}`);
|
|
66
26
|
}
|
|
67
27
|
|
|
68
28
|
const data = await response.json();
|
|
69
|
-
|
|
70
|
-
if (signal?.aborted) {
|
|
71
|
-
throw new DOMException("The operation was aborted.", "AbortError");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
29
|
const dataToValidate = extractData ? extractData(data) : data;
|
|
75
|
-
|
|
76
30
|
const validationResult = schema.safeParse(dataToValidate);
|
|
77
31
|
|
|
78
32
|
if (!validationResult.success) {
|
|
@@ -81,14 +35,6 @@ export async function fetchAndValidate<T>(
|
|
|
81
35
|
|
|
82
36
|
return validationResult.data;
|
|
83
37
|
} catch (error) {
|
|
84
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
38
|
if (error instanceof Error && error.name === "ZodError") {
|
|
93
39
|
throw new Error(`Invalid ${errorContext} response format: ${error.message}`);
|
|
94
40
|
}
|
|
@@ -108,18 +54,6 @@ export async function fetchAndValidate<T>(
|
|
|
108
54
|
}
|
|
109
55
|
}
|
|
110
56
|
|
|
111
|
-
/**
|
|
112
|
-
* Helper to safely encode path components for URLs
|
|
113
|
-
* Wraps encodeURIComponent for better semantic meaning
|
|
114
|
-
*
|
|
115
|
-
* @param segment - The path segment to encode
|
|
116
|
-
* @returns URL-encoded path segment
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* ```tsx
|
|
120
|
-
* const safePath = safeEncodePath('Account Name'); // 'Account%20Name'
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
57
|
export function safeEncodePath(segment: string): string {
|
|
124
58
|
return encodeURIComponent(segment);
|
|
125
59
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API for the Global Search feature package.
|
|
3
|
+
*
|
|
4
|
+
* Design goals:
|
|
5
|
+
* - Export **API services, hooks, types, schemas, and utilities** that customers can import from node_modules.
|
|
6
|
+
* - Do **not** export UI components or feature constants (customers build their own UI).
|
|
7
|
+
*
|
|
8
|
+
* Source implementation lives under `src/features/global-search/**`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// API layer
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export { objectInfoService } from "./features/global-search/api/objectInfoService";
|
|
16
|
+
export {
|
|
17
|
+
objectDetailService,
|
|
18
|
+
extractFieldsFromLayout,
|
|
19
|
+
} from "./features/global-search/api/objectDetailService";
|
|
20
|
+
export type { RecordDetailResult } from "./features/global-search/api/objectDetailService";
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
getRecordsGraphQL,
|
|
24
|
+
getRecordByIdGraphQL,
|
|
25
|
+
buildGetRecordsQuery,
|
|
26
|
+
buildWhereFromCriteria,
|
|
27
|
+
buildOrderByFromSort,
|
|
28
|
+
} from "./features/global-search/api/recordListGraphQLService";
|
|
29
|
+
export type {
|
|
30
|
+
RecordListGraphQLResult,
|
|
31
|
+
RecordListGraphQLVariables,
|
|
32
|
+
RecordListGraphQLOptions,
|
|
33
|
+
GraphQLRecordNode,
|
|
34
|
+
} from "./features/global-search/api/recordListGraphQLService";
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Hooks
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
export { useObjectInfoBatch } from "./features/global-search/hooks/useObjectInfoBatch";
|
|
41
|
+
export {
|
|
42
|
+
useObjectListMetadata,
|
|
43
|
+
useObjectColumns,
|
|
44
|
+
useObjectFilters,
|
|
45
|
+
} from "./features/global-search/hooks/useObjectSearchData";
|
|
46
|
+
export { useRecordListGraphQL } from "./features/global-search/hooks/useRecordListGraphQL";
|
|
47
|
+
export { useRecordDetailLayout } from "./features/global-search/hooks/useRecordDetailLayout";
|
|
48
|
+
|
|
49
|
+
export type { ObjectListMetadata } from "./features/global-search/hooks/useObjectSearchData";
|
|
50
|
+
|
|
51
|
+
export type {
|
|
52
|
+
UseRecordListGraphQLOptions,
|
|
53
|
+
UseRecordListGraphQLReturn,
|
|
54
|
+
} from "./features/global-search/hooks/useRecordListGraphQL";
|
|
55
|
+
|
|
56
|
+
export type {
|
|
57
|
+
UseRecordDetailLayoutParams,
|
|
58
|
+
UseRecordDetailLayoutReturn,
|
|
59
|
+
} from "./features/global-search/hooks/useRecordDetailLayout";
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Types + Zod schemas (runtime validation)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
ColumnArraySchema,
|
|
67
|
+
SearchResultRecordArraySchema,
|
|
68
|
+
KeywordSearchResultSchema,
|
|
69
|
+
SearchResultsResponseSchema,
|
|
70
|
+
} from "./features/global-search/types/search/searchResults";
|
|
71
|
+
export type {
|
|
72
|
+
Column,
|
|
73
|
+
SearchResultRecord,
|
|
74
|
+
KeywordSearchResult,
|
|
75
|
+
SearchResultsResponse,
|
|
76
|
+
} from "./features/global-search/types/search/searchResults";
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
FilterArraySchema,
|
|
80
|
+
FilterCriteriaArraySchema,
|
|
81
|
+
FILTER_OPERATORS,
|
|
82
|
+
} from "./features/global-search/types/filters/filters";
|
|
83
|
+
export type {
|
|
84
|
+
Filter,
|
|
85
|
+
FilterCriteria,
|
|
86
|
+
FilterOperator,
|
|
87
|
+
FiltersResponse,
|
|
88
|
+
} from "./features/global-search/types/filters/filters";
|
|
89
|
+
|
|
90
|
+
export { PicklistValueArraySchema } from "./features/global-search/types/filters/picklist";
|
|
91
|
+
export type { PicklistValue } from "./features/global-search/types/filters/picklist";
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
ObjectInfoBatchResponseSchema,
|
|
95
|
+
ObjectInfoResultSchema,
|
|
96
|
+
} from "./features/global-search/types/objectInfo/objectInfo";
|
|
97
|
+
export type {
|
|
98
|
+
ObjectInfoBatchResponse,
|
|
99
|
+
ObjectInfoResult,
|
|
100
|
+
} from "./features/global-search/types/objectInfo/objectInfo";
|
|
101
|
+
|
|
102
|
+
export { LayoutResponseSchema } from "./features/global-search/types/recordDetail/recordDetail";
|
|
103
|
+
export type { LayoutResponse } from "./features/global-search/types/recordDetail/recordDetail";
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Utilities
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
export { fetchAndValidate, safeEncodePath } from "./features/global-search/utils/apiUtils";
|
|
110
|
+
export { debounce } from "./features/global-search/utils/debounce";
|
|
111
|
+
export { createFiltersKey } from "./features/global-search/utils/cacheUtils";
|
|
112
|
+
export {
|
|
113
|
+
calculateFieldsToFetch,
|
|
114
|
+
getSafeKey,
|
|
115
|
+
isValidSalesforceId,
|
|
116
|
+
} from "./features/global-search/utils/recordUtils";
|
|
117
|
+
export { parseFilterValue } from "./features/global-search/utils/filterUtils";
|
|
118
|
+
export { sanitizeFilterValue } from "./features/global-search/utils/sanitizationUtils";
|
|
119
|
+
export {
|
|
120
|
+
getGraphQLNodeValue,
|
|
121
|
+
getDisplayValueForDetailFieldFromNode,
|
|
122
|
+
getDisplayValueForLayoutItemFromNode,
|
|
123
|
+
getGraphQLRecordDisplayName,
|
|
124
|
+
} from "./features/global-search/utils/graphQLNodeFieldUtils";
|
|
125
|
+
export { graphQLNodeToSearchResultRecordData } from "./features/global-search/utils/graphQLRecordAdapter";
|
package/dist/package.json
CHANGED