@salesforce/webapp-template-app-react-sample-b2x-experimental 1.93.1 → 1.94.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.
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: webapplications-features
2
+ name: webapp-features
3
3
  description: Search, describe, and install pre-built UI features (authentication, shadcn components, navigation, charts, search, GraphQL, Agentforce AI) into Salesforce webapps. Use this when the user wants to add functionality to a webapp, or when determining what salesforce-provided features are available — whether prompted by the user or on your own initiative. Always check for an existing feature before building from scratch.
4
4
  ---
5
5
 
@@ -71,8 +71,8 @@ Resolves the feature name to an npm package, installs it and its dependencies (i
71
71
 
72
72
  Options:
73
73
 
74
- - `--webapp-dir <path>` (required) — Path to the webapp directory
75
- - `--sfdx-root <path>` (default: `force-app/main/default`) — SFDX metadata root directory
74
+ - `--webapp-dir <name>` (required) — Webapp name, resolves to `<sfdx-source>/webapplications/<name>`
75
+ - `--sfdx-source <path>` (default: `force-app/main/default`) — SFDX source directory
76
76
  - `--dry-run` (default: `false`) — Preview changes without writing files
77
77
  - `-v, --verbose` (default: `false`) — Enable verbose logging
78
78
  - `-y, --yes` (default: `false`) — Skip all prompts (auto-skip conflicts)
@@ -82,16 +82,16 @@ Options:
82
82
  ```bash
83
83
  # Install authentication (also installs shadcn dependency)
84
84
  npx @salesforce/webapps-features-experimental install authentication \
85
- --webapp-dir force-app/main/default/webapplications/mywebapp
85
+ --webapp-dir mywebapp
86
86
 
87
87
  # Dry run to preview changes
88
88
  npx @salesforce/webapps-features-experimental install shadcn \
89
- --webapp-dir force-app/main/default/webapplications/mywebapp \
89
+ --webapp-dir mywebapp \
90
90
  --dry-run
91
91
 
92
92
  # Non-interactive install (skip all file conflicts)
93
93
  npx @salesforce/webapps-features-experimental install authentication \
94
- --webapp-dir force-app/main/default/webapplications/mywebapp \
94
+ --webapp-dir mywebapp \
95
95
  --yes
96
96
  ```
97
97
 
@@ -106,7 +106,7 @@ Since you are running in a non-interactive environment, you cannot use `--on-con
106
106
  ```bash
107
107
  # Pass 1: detect conflicts
108
108
  npx @salesforce/webapps-features-experimental install authentication \
109
- --webapp-dir force-app/main/default/webapplications/mywebapp \
109
+ --webapp-dir mywebapp \
110
110
  --on-conflict error
111
111
 
112
112
  # The CLI will exit with an error listing every conflicting file path.
@@ -115,7 +115,7 @@ npx @salesforce/webapps-features-experimental install authentication \
115
115
  echo '{ "src/styles/global.css": "overwrite", "src/lib/utils.ts": "skip" }' > resolutions.json
116
116
 
117
117
  npx @salesforce/webapps-features-experimental install authentication \
118
- --webapp-dir force-app/main/default/webapplications/mywebapp \
118
+ --webapp-dir mywebapp \
119
119
  --conflict-resolution resolutions.json
120
120
  ```
121
121
 
@@ -131,7 +131,7 @@ Some copy operations use **hint placeholders** in the `"to"` path — descriptiv
131
131
  2. Rename or relocate it to the intended target (e.g., `src/pages/Home.tsx`)
132
132
  3. Or integrate its patterns into an existing file, then delete it
133
133
 
134
- **How to identify them:** Hint placeholders use `<descriptive-name>` syntax but are NOT one of the system placeholders (`<sfdxRoot>`, `<webappDir>`, `<webapp>`). They always appear in the middle or end of a path, never as the leading segment.
134
+ **How to identify them:** Hint placeholders use `<descriptive-name>` syntax but are NOT one of the system placeholders (`<sfdxSource>`, `<webappDir>`, `<webapp>`). They always appear in the middle or end of a path, never as the leading segment.
135
135
 
136
136
  **Example from features.json:**
137
137
 
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.94.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.94.0...v1.94.1) (2026-03-12)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * adjust features cli @W-21452399 ([#258](https://github.com/salesforce-experience-platform-emu/webapps/issues/258)) ([524abe8](https://github.com/salesforce-experience-platform-emu/webapps/commit/524abe853684a770777b00238432a01d60dd3366))
12
+
13
+
14
+
15
+
16
+
17
+ # [1.94.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.93.1...v1.94.0) (2026-03-12)
18
+
19
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
20
+
21
+
22
+
23
+
24
+
6
25
  ## [1.93.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.93.0...v1.93.1) (2026-03-12)
7
26
 
8
27
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -15,10 +15,10 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.93.1",
19
- "@salesforce/webapp-experimental": "^1.93.1",
18
+ "@salesforce/sdk-data": "^1.94.1",
19
+ "@salesforce/webapp-experimental": "^1.94.1",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
- "@tanstack/react-form": "^1.28.4",
21
+ "@tanstack/react-form": "^1.28.5",
22
22
  "@types/leaflet": "^1.9.21",
23
23
  "class-variance-authority": "^0.7.1",
24
24
  "clsx": "^2.1.1",
@@ -43,7 +43,7 @@
43
43
  "@graphql-eslint/eslint-plugin": "^4.1.0",
44
44
  "@graphql-tools/utils": "^11.0.0",
45
45
  "@playwright/test": "^1.49.0",
46
- "@salesforce/vite-plugin-webapp-experimental": "^1.93.1",
46
+ "@salesforce/vite-plugin-webapp-experimental": "^1.94.1",
47
47
  "@testing-library/jest-dom": "^6.6.3",
48
48
  "@testing-library/react": "^16.1.0",
49
49
  "@testing-library/user-event": "^14.5.2",
@@ -1,22 +1,31 @@
1
1
  import { Link, useLocation } from "react-router";
2
2
  import { Home, Search, BarChart3, Wrench, Phone, type LucideIcon } from "lucide-react";
3
+ import { useAuth } from "@/features/authentication/context/AuthContext";
4
+ import { useMemo } from "react";
3
5
 
4
6
  interface NavItem {
5
7
  path: string;
6
8
  icon: LucideIcon;
7
9
  label: string;
10
+ authRequired?: boolean;
8
11
  }
9
12
 
10
13
  const navItems: NavItem[] = [
11
14
  { path: "/", icon: Home, label: "Home" },
12
- { path: "/dashboard", icon: BarChart3, label: "Dashboard" },
15
+ { path: "/dashboard", icon: BarChart3, label: "Dashboard", authRequired: true },
13
16
  { path: "/properties", icon: Search, label: "Property Search" },
14
- { path: "/maintenance/requests", icon: Wrench, label: "Maintenance Requests" },
17
+ { path: "/maintenance", icon: Wrench, label: "Maintenance Requests", authRequired: true },
15
18
  { path: "/contact", icon: Phone, label: "Contact Us" },
16
19
  ];
17
20
 
18
21
  export function NavMenu() {
19
22
  const location = useLocation();
23
+ const { isAuthenticated } = useAuth();
24
+
25
+ const visibleItems = useMemo(
26
+ () => navItems.filter((item) => !item.authRequired || isAuthenticated),
27
+ [isAuthenticated],
28
+ );
20
29
 
21
30
  const isActive = (path: string) => {
22
31
  if (path === "/") return location.pathname === "/";
@@ -28,7 +37,7 @@ export function NavMenu() {
28
37
  className="flex w-24 flex-col border-r border-gray-200 bg-white py-8"
29
38
  aria-label="Main navigation"
30
39
  >
31
- {navItems.map((item) => {
40
+ {visibleItems.map((item) => {
32
41
  const Icon = item.icon;
33
42
  const active = isActive(item.path);
34
43
  return (
@@ -1,6 +1,8 @@
1
- import { useEffect, useState } from "react";
2
- import { Search, Bell, ChevronDown, Menu } from "lucide-react";
3
- import { getUserInfo } from "@/api/userApi";
1
+ import { useState } from "react";
2
+ import { Search, Bell, ChevronDown, Menu, UserPen, LogOut, User, Loader2 } from "lucide-react";
3
+ import { Link } from "react-router";
4
+ import { useAuth } from "../features/authentication/context/AuthContext";
5
+ import { ROUTES } from "../features/authentication/authenticationConfig";
4
6
  import zenLogo from "@/assets/icons/zen-logo.svg";
5
7
 
6
8
  export interface TopBarProps {
@@ -8,26 +10,7 @@ export interface TopBarProps {
8
10
  }
9
11
 
10
12
  export function TopBar({ onMenuClick }: TopBarProps) {
11
- const [userName, setUserName] = useState<string>("User");
12
- const [showNotifications, setShowNotifications] = useState(false);
13
-
14
- useEffect(() => {
15
- const loadUserInfo = async () => {
16
- const userInfo = await getUserInfo();
17
- if (userInfo) {
18
- setUserName(userInfo.name);
19
- }
20
- };
21
- loadUserInfo();
22
- }, []);
23
-
24
- const handleNotificationClick = () => {
25
- setShowNotifications(!showNotifications);
26
- };
27
-
28
- const handleCloseNotifications = () => {
29
- setShowNotifications(false);
30
- };
13
+ const { user, isAuthenticated, loading } = useAuth();
31
14
 
32
15
  return (
33
16
  <header
@@ -43,65 +26,149 @@ export function TopBar({ onMenuClick }: TopBarProps) {
43
26
  >
44
27
  <Menu className="size-6" aria-hidden />
45
28
  </button>
46
- <div className="flex items-center gap-2">
47
- <img src={zenLogo} alt="Zenlease Logo" className="size-8" />
48
- <span className="text-xl tracking-wide">
49
- <span className="font-light">ZEN</span>
50
- <span className="font-semibold">LEASE</span>
51
- </span>
52
- </div>
29
+ <Logo />
53
30
  </div>
54
31
 
55
- <div className="flex items-center gap-4">
56
- <button
57
- type="button"
58
- className="rounded-md p-2 transition-colors hover:bg-teal-600 md:hidden"
59
- aria-label="Search"
60
- >
61
- <Search className="size-5" aria-hidden />
62
- </button>
32
+ {loading ? (
33
+ <Loader2 className="size-5 animate-spin" aria-label="Loading" />
34
+ ) : isAuthenticated ? (
35
+ <AuthenticatedControls userName={user?.name ?? "User"} />
36
+ ) : (
37
+ <LoginLink />
38
+ )}
39
+ </header>
40
+ );
41
+ }
63
42
 
64
- <div className="relative">
65
- <button
66
- type="button"
67
- onClick={handleNotificationClick}
68
- className="relative rounded-md p-2 transition-colors hover:bg-teal-600"
69
- aria-label="Notifications"
70
- >
71
- <Bell className="size-5" aria-hidden />
72
- </button>
43
+ function Logo() {
44
+ return (
45
+ <div className="flex items-center gap-2">
46
+ <img src={zenLogo} alt="Zenlease Logo" className="size-8" />
47
+ <span className="text-xl tracking-wide">
48
+ <span className="font-light">ZEN</span>
49
+ <span className="font-semibold">LEASE</span>
50
+ </span>
51
+ </div>
52
+ );
53
+ }
73
54
 
74
- {showNotifications && (
75
- <>
76
- <div className="fixed inset-0 z-40" onClick={handleCloseNotifications} aria-hidden />
77
- <div className="absolute right-0 top-full z-50 mt-2 w-80 overflow-hidden rounded-lg bg-white shadow-xl">
78
- <div className="border-b border-gray-200 p-4">
79
- <h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
80
- </div>
81
- <div className="p-8 text-center">
82
- <Bell className="mx-auto mb-3 size-12 text-gray-300" aria-hidden />
83
- <p className="text-sm text-gray-500">No new notifications</p>
84
- </div>
85
- </div>
86
- </>
87
- )}
88
- </div>
55
+ function LoginLink() {
56
+ return (
57
+ <a
58
+ href={ROUTES.LOGIN.PATH}
59
+ className="flex items-center gap-2 rounded-md bg-white px-4 py-2 font-medium text-teal-700 transition-colors hover:bg-teal-50"
60
+ >
61
+ <User className="size-4" aria-hidden />
62
+ Sign Up / Sign In
63
+ </a>
64
+ );
65
+ }
89
66
 
90
- <button
91
- type="button"
92
- className="flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-teal-600"
93
- aria-label="User menu"
67
+ function AuthenticatedControls({ userName }: { userName: string }) {
68
+ return (
69
+ <div className="flex items-center gap-4">
70
+ <button
71
+ type="button"
72
+ className="rounded-md p-2 transition-colors hover:bg-teal-600 md:hidden"
73
+ aria-label="Search"
74
+ >
75
+ <Search className="size-5" aria-hidden />
76
+ </button>
77
+
78
+ <NotificationBell />
79
+ <UserMenu userName={userName} />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ function UserMenu({ userName }: { userName: string }) {
85
+ const { logout } = useAuth();
86
+ const [open, setOpen] = useState(false);
87
+
88
+ return (
89
+ <div className="relative">
90
+ <button
91
+ type="button"
92
+ onClick={() => setOpen((prev) => !prev)}
93
+ className="flex items-center gap-2 rounded-md px-3 py-2 transition-colors hover:bg-teal-600"
94
+ aria-label="User menu"
95
+ aria-expanded={open}
96
+ aria-haspopup="true"
97
+ >
98
+ <div
99
+ className="flex size-8 items-center justify-center rounded-full bg-teal-300 font-semibold text-teal-900"
100
+ aria-hidden
94
101
  >
102
+ {userName.charAt(0).toUpperCase()}
103
+ </div>
104
+ <span className="hidden font-medium md:inline">{userName.toUpperCase()}</span>
105
+ <ChevronDown className="hidden size-4 md:inline" aria-hidden />
106
+ </button>
107
+
108
+ {open && (
109
+ <>
110
+ <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} aria-hidden />
95
111
  <div
96
- className="flex size-8 items-center justify-center rounded-full bg-teal-300 font-semibold text-teal-900"
97
- aria-hidden
112
+ className="absolute right-0 top-full z-50 mt-2 w-48 overflow-hidden rounded-lg bg-white py-1 shadow-xl"
113
+ role="menu"
98
114
  >
99
- {userName.charAt(0).toUpperCase()}
115
+ <Link
116
+ to={ROUTES.PROFILE.PATH}
117
+ onClick={() => setOpen(false)}
118
+ className="flex items-center gap-3 px-4 py-2.5 text-sm text-gray-700 transition-colors hover:bg-gray-100"
119
+ role="menuitem"
120
+ >
121
+ <UserPen className="size-4" aria-hidden />
122
+ Edit Profile
123
+ </Link>
124
+ <div className="mx-3 border-t border-gray-200" />
125
+ <button
126
+ type="button"
127
+ onClick={() => {
128
+ setOpen(false);
129
+ logout();
130
+ }}
131
+ className="flex w-full items-center gap-3 px-4 py-2.5 text-sm text-gray-700 transition-colors hover:bg-gray-100"
132
+ role="menuitem"
133
+ >
134
+ <LogOut className="size-4" aria-hidden />
135
+ Log Out
136
+ </button>
100
137
  </div>
101
- <span className="hidden font-medium md:inline">{userName.toUpperCase()}</span>
102
- <ChevronDown className="hidden size-4 md:inline" aria-hidden />
103
- </button>
104
- </div>
105
- </header>
138
+ </>
139
+ )}
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function NotificationBell() {
145
+ const [open, setOpen] = useState(false);
146
+
147
+ return (
148
+ <div className="relative">
149
+ <button
150
+ type="button"
151
+ onClick={() => setOpen((prev) => !prev)}
152
+ className="relative rounded-md p-2 transition-colors hover:bg-teal-600"
153
+ aria-label="Notifications"
154
+ >
155
+ <Bell className="size-5" aria-hidden />
156
+ </button>
157
+
158
+ {open && (
159
+ <>
160
+ <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} aria-hidden />
161
+ <div className="absolute right-0 top-full z-50 mt-2 w-80 overflow-hidden rounded-lg bg-white shadow-xl">
162
+ <div className="border-b border-gray-200 p-4">
163
+ <h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
164
+ </div>
165
+ <div className="p-8 text-center">
166
+ <Bell className="mx-auto mb-3 size-12 text-gray-300" aria-hidden />
167
+ <p className="text-sm text-gray-500">No new notifications</p>
168
+ </div>
169
+ </div>
170
+ </>
171
+ )}
172
+ </div>
106
173
  );
107
174
  }
@@ -29,7 +29,7 @@ export default function ChangePassword() {
29
29
  // [Dev Note] Custom Apex Endpoint: /auth/change-password
30
30
  // You must ensure this Apex class exists in your org
31
31
  const sdk = await getDataSDK();
32
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/change-password", {
32
+ const response = await sdk.fetch!("/services/apexrest/auth/change-password", {
33
33
  method: "POST",
34
34
  body: JSON.stringify({
35
35
  currentPassword: formFieldValues.currentPassword,
@@ -25,7 +25,7 @@ export default function ForgotPassword() {
25
25
  // [Dev Note] Custom Apex Endpoint: /auth/forgot-password
26
26
  // You must ensure this Apex class exists in your org
27
27
  const sdk = await getDataSDK();
28
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/forgot-password", {
28
+ const response = await sdk.fetch!("/services/apexrest/auth/forgot-password", {
29
29
  method: "POST",
30
30
  body: JSON.stringify({ username: value.username.trim() }),
31
31
  headers: {
@@ -27,11 +27,11 @@ export default function Login() {
27
27
  try {
28
28
  // [Dev Note] Salesforce Integration:
29
29
  // We use the Data SDK fetch to make an authenticated (or guest) call to Salesforce.
30
- // "/sfdcapi/services/apexrest/auth/login" refers to a custom Apex REST resource.
30
+ // "/services/apexrest/auth/login" refers to a custom Apex REST resource.
31
31
  // You must ensure this Apex class exists in your org and handles the login logic
32
32
  // (e.g., creating a session or returning a token).
33
33
  const sdk = await getDataSDK();
34
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/login", {
34
+ const response = await sdk.fetch!("/services/apexrest/auth/login", {
35
35
  method: "POST",
36
36
  body: JSON.stringify({
37
37
  email: value.email.trim().toLowerCase(),
@@ -43,12 +43,12 @@ export default function Register() {
43
43
  try {
44
44
  // [Dev Note] Salesforce Integration:
45
45
  // We use the Data SDK fetch to make an authenticated (or guest) call to Salesforce.
46
- // "/sfdcapi/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
46
+ // "/services/apexrest/auth/register" refers to a custom Apex Class exposed as a REST resource.
47
47
  // You must ensure this Apex class exists in your org and handles registration
48
48
  // (e.g., duplicate checks and user creation such as Site.createExternalUser).
49
49
  const { confirmPassword, ...request } = formFieldValues;
50
50
  const sdk = await getDataSDK();
51
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/register", {
51
+ const response = await sdk.fetch!("/services/apexrest/auth/register", {
52
52
  method: "POST",
53
53
  body: JSON.stringify({ request }),
54
54
  headers: {
@@ -26,7 +26,7 @@ export default function ResetPassword() {
26
26
  // [Dev Note] Custom Apex Endpoint: /auth/reset-password
27
27
  // You must ensure this Apex class exists in your org
28
28
  const sdk = await getDataSDK();
29
- const response = await sdk.fetch!("/sfdcapi/services/apexrest/auth/reset-password", {
29
+ const response = await sdk.fetch!("/services/apexrest/auth/reset-password", {
30
30
  method: "POST",
31
31
  body: JSON.stringify({ token, newPassword: value.newPassword }),
32
32
  headers: {
@@ -107,15 +107,6 @@ export const routes: RouteObject[] = [
107
107
  path: "object/Property_Listing__c/:id",
108
108
  element: <PropertyDetails />
109
109
  },
110
- {
111
- path: "maintenance/requests",
112
- element: <Maintenance />,
113
- handle: { showInNavigation: true, label: "Maintenance" }
114
- },
115
- {
116
- path: "application",
117
- element: <Application />
118
- },
119
110
  {
120
111
  path: "contact",
121
112
  element: <Contact />,
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.93.1",
3
+ "version": "1.94.1",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-app-react-sample-b2x-experimental",
3
- "version": "1.93.1",
3
+ "version": "1.94.1",
4
4
  "description": "B2C sample app template with app shell",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
@@ -1,43 +0,0 @@
1
- import { gql } from "@salesforce/sdk-data";
2
- import type { GetUserInfoQuery } from "@/api/graphql-operations-types.js";
3
- import { executeGraphQL } from "@/api/graphqlClient.js";
4
-
5
- const GET_USER_INFO = gql`
6
- query GetUserInfo {
7
- uiapi {
8
- query {
9
- User(first: 1) {
10
- edges {
11
- node {
12
- Id
13
- Name {
14
- value
15
- }
16
- }
17
- }
18
- }
19
- }
20
- }
21
- }
22
- `;
23
-
24
- /**
25
- * Fetches the current user's id and name (for TopBar, etc.).
26
- * Returns null on error or when no user is returned.
27
- */
28
- export async function getUserInfo(): Promise<{ name: string; id: string } | null> {
29
- try {
30
- const data = await executeGraphQL<GetUserInfoQuery>(GET_USER_INFO);
31
- const user = data?.uiapi?.query?.User?.edges?.[0]?.node;
32
- if (user) {
33
- return {
34
- id: user.Id,
35
- name: user.Name?.value ?? "User",
36
- };
37
- }
38
- return null;
39
- } catch (error) {
40
- console.error("Error fetching user info:", error);
41
- return null;
42
- }
43
- }