@salesforce/ui-bundle-template-app-react-sample-b2e 12.8.0 → 12.9.0
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/CHANGELOG.md +8 -0
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/package.json +4 -4
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/PaginationControls.tsx +1 -5
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/results/CmsResultRow.tsx +49 -4
- package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/results/__tests__/CmsResultRow.test.tsx +56 -0
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +2 -2
package/dist/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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
|
+
## [12.9.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.8.0...v12.9.0) (2026-09-29)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
## [12.8.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v12.7.0...v12.8.0) (2026-09-23)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"graphql:schema": "node scripts/get-graphql-schema.mjs"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@salesforce/platform-sdk": "^12.
|
|
23
|
-
"@salesforce/ui-bundle": "^12.
|
|
22
|
+
"@salesforce/platform-sdk": "^12.9.0",
|
|
23
|
+
"@salesforce/ui-bundle": "^12.9.0",
|
|
24
24
|
"@tailwindcss/vite": "^4.1.17",
|
|
25
25
|
"class-variance-authority": "^0.7.1",
|
|
26
26
|
"clsx": "^2.1.1",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"@graphql-eslint/eslint-plugin": "^4.1.0",
|
|
48
48
|
"@graphql-tools/utils": "^11.0.0",
|
|
49
49
|
"@playwright/test": "^1.49.0",
|
|
50
|
-
"@salesforce/graphiti": "^12.
|
|
51
|
-
"@salesforce/vite-plugin-ui-bundle": "^12.
|
|
50
|
+
"@salesforce/graphiti": "^12.9.0",
|
|
51
|
+
"@salesforce/vite-plugin-ui-bundle": "^12.9.0",
|
|
52
52
|
"@testing-library/jest-dom": "^6.6.3",
|
|
53
53
|
"@testing-library/react": "^16.1.0",
|
|
54
54
|
"@testing-library/user-event": "^14.5.2",
|
|
@@ -107,11 +107,7 @@ export function PaginationControls({
|
|
|
107
107
|
<Label htmlFor={selectId} className="text-sm font-normal whitespace-nowrap">
|
|
108
108
|
Results per page:
|
|
109
109
|
</Label>
|
|
110
|
-
<Select
|
|
111
|
-
value={pageSize.toString()}
|
|
112
|
-
onValueChange={handlePageSizeChange}
|
|
113
|
-
disabled={disabled}
|
|
114
|
-
>
|
|
110
|
+
<Select value={pageSize.toString()} onValueChange={handlePageSizeChange}>
|
|
115
111
|
<SelectTrigger id={selectId} className="w-16">
|
|
116
112
|
<SelectValue />
|
|
117
113
|
</SelectTrigger>
|
|
@@ -20,6 +20,11 @@ import { Link } from "react-router";
|
|
|
20
20
|
import type { CmsSourceConfig, CmsSearchItem } from "../../adapters/cms/types";
|
|
21
21
|
import { formatContentTypeLabel, isValidCmsFqn } from "../../adapters/cms/contentTypeUtils";
|
|
22
22
|
|
|
23
|
+
const EM_BLOCK_SPLIT = /(<(?:em)>[\s\S]*?<\/(?:em)>)/g;
|
|
24
|
+
const EM_BLOCK_EXEC = /^<(?:em)>([\s\S]*?)<\/(?:em)>$/;
|
|
25
|
+
const STRAY_TAG = /<\/?[a-zA-Z][^>]*>/g;
|
|
26
|
+
const ROUTE_TOKEN = /:([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
27
|
+
|
|
23
28
|
/** Formats an ISO date string for display; echoes the raw string on failure. */
|
|
24
29
|
function formatPublishedDate(dateStr: string): string {
|
|
25
30
|
const date = new Date(dateStr);
|
|
@@ -43,7 +48,7 @@ function resolveRoute(pattern: string, item: CmsSearchItem): string | null {
|
|
|
43
48
|
contentType: item.contentType,
|
|
44
49
|
};
|
|
45
50
|
let failed = false;
|
|
46
|
-
const out = pattern.replace(
|
|
51
|
+
const out = pattern.replace(ROUTE_TOKEN, (_match, token: string) => {
|
|
47
52
|
const value = tokens[token];
|
|
48
53
|
if (value == null || value === "") {
|
|
49
54
|
failed = true;
|
|
@@ -54,6 +59,44 @@ function resolveRoute(pattern: string, item: CmsSearchItem): string | null {
|
|
|
54
59
|
return failed ? null : out;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
/**
|
|
63
|
+
* - Decodes every HTML entity the browser knows (named + numeric).
|
|
64
|
+
* - Runs twice to unwrap the double-encoding CMS search sometimes returns.
|
|
65
|
+
*/
|
|
66
|
+
function decodeEntities(input: string): string {
|
|
67
|
+
if (typeof document === "undefined") return input;
|
|
68
|
+
const el = document.createElement("textarea");
|
|
69
|
+
const pass = (s: string) => {
|
|
70
|
+
el.innerHTML = s;
|
|
71
|
+
return el.value;
|
|
72
|
+
};
|
|
73
|
+
return pass(pass(input));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parses a snippet that may contain `<em>…</em>` segments and renders it as highlighted text,
|
|
78
|
+
* and strips any other HTML tags so their content renders as plain text.
|
|
79
|
+
*/
|
|
80
|
+
function renderHighlightedSnippet(snippet: string): ReactNode[] {
|
|
81
|
+
const decoded = decodeEntities(snippet);
|
|
82
|
+
const parts = decoded.split(EM_BLOCK_SPLIT);
|
|
83
|
+
return parts.map((part, i) => {
|
|
84
|
+
const emphasisMatch = EM_BLOCK_EXEC.exec(part);
|
|
85
|
+
if (emphasisMatch) {
|
|
86
|
+
return (
|
|
87
|
+
<mark key={i} className="bg-yellow-200 dark:bg-yellow-700/60 rounded-sm px-0.5">
|
|
88
|
+
{emphasisMatch[1].replace(STRAY_TAG, "")}
|
|
89
|
+
</mark>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return part.replace(STRAY_TAG, "");
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toPlainText(raw: string): string {
|
|
97
|
+
return decodeEntities(raw).replace(STRAY_TAG, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
57
100
|
interface CmsResultRowProps {
|
|
58
101
|
node: unknown;
|
|
59
102
|
source: CmsSourceConfig;
|
|
@@ -71,9 +114,11 @@ export function CmsResultRow({ node, source }: CmsResultRowProps) {
|
|
|
71
114
|
|
|
72
115
|
const content: ReactNode = (
|
|
73
116
|
<>
|
|
74
|
-
<span className="font-medium">{item.title
|
|
117
|
+
<span className="font-medium">{item.title ? renderHighlightedSnippet(item.title) : "—"}</span>
|
|
75
118
|
{item.highlightedSnippet && (
|
|
76
|
-
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
|
119
|
+
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
|
120
|
+
{renderHighlightedSnippet(item.highlightedSnippet)}
|
|
121
|
+
</p>
|
|
77
122
|
)}
|
|
78
123
|
{subtitleParts.length > 0 && (
|
|
79
124
|
<p className="text-sm text-neutral-600 dark:text-neutral-300">
|
|
@@ -92,7 +137,7 @@ export function CmsResultRow({ node, source }: CmsResultRowProps) {
|
|
|
92
137
|
<Link
|
|
93
138
|
to={href}
|
|
94
139
|
className="block hover:bg-accent rounded-md px-3 -mx-3 py-1 transition-colors"
|
|
95
|
-
aria-label={`View content: ${item.title
|
|
140
|
+
aria-label={`View content: ${item.title ? toPlainText(item.title) : "content"}`}
|
|
96
141
|
>
|
|
97
142
|
{content}
|
|
98
143
|
</Link>
|
|
@@ -108,6 +108,62 @@ describe("CmsResultRow", () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
describe("highlight and entity decoding", () => {
|
|
112
|
+
it("converts <em> blocks to <mark> elements", () => {
|
|
113
|
+
const item: CmsSearchItem = {
|
|
114
|
+
...baseItem,
|
|
115
|
+
highlightedSnippet: "the <em>king</em> returns",
|
|
116
|
+
};
|
|
117
|
+
const container = renderWithRouter(<CmsResultRow node={item} source={source} />);
|
|
118
|
+
|
|
119
|
+
const mark = container.querySelector("mark");
|
|
120
|
+
expect(mark).toBeTruthy();
|
|
121
|
+
expect(mark?.textContent).toBe("king");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("decodes double-encoded entities from CMS search", () => {
|
|
125
|
+
const item: CmsSearchItem = {
|
|
126
|
+
...baseItem,
|
|
127
|
+
highlightedSnippet: "&#39;<em>king</em>&#39;",
|
|
128
|
+
};
|
|
129
|
+
const container = renderWithRouter(<CmsResultRow node={item} source={source} />);
|
|
130
|
+
|
|
131
|
+
expect(container.textContent).toContain("'king'");
|
|
132
|
+
expect(container.textContent).not.toContain("&");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("strips stray HTML tags but keeps their text content", () => {
|
|
136
|
+
const item: CmsSearchItem = {
|
|
137
|
+
...baseItem,
|
|
138
|
+
highlightedSnippet: "<em>hi<b>bold</b>there</em> <p>plain</p>",
|
|
139
|
+
};
|
|
140
|
+
const container = renderWithRouter(<CmsResultRow node={item} source={source} />);
|
|
141
|
+
|
|
142
|
+
expect(container.querySelector("mark")?.textContent).toBe("hiboldthere");
|
|
143
|
+
expect(container.innerHTML).not.toContain("<b>");
|
|
144
|
+
expect(container.innerHTML).not.toContain("<p>");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("handles the real CMS response pattern end-to-end", () => {
|
|
148
|
+
const item: CmsSearchItem = {
|
|
149
|
+
...baseItem,
|
|
150
|
+
title: "Inside the '<em>King</em>' — What's Going On",
|
|
151
|
+
highlightedSnippet: "Inside the &#39;<em>king</em>&#39; of the industry.",
|
|
152
|
+
};
|
|
153
|
+
const container = renderWithRouter(<CmsResultRow node={item} source={source} />);
|
|
154
|
+
|
|
155
|
+
expect(container.querySelector("span.font-medium")?.textContent).toContain(
|
|
156
|
+
"Inside the 'King' — What's Going On",
|
|
157
|
+
);
|
|
158
|
+
expect(container.querySelector("span.font-medium mark")?.textContent).toBe("King");
|
|
159
|
+
const snippet = Array.from(container.querySelectorAll("p")).find((p) =>
|
|
160
|
+
p.textContent?.includes("industry"),
|
|
161
|
+
);
|
|
162
|
+
expect(snippet?.querySelector("mark")?.textContent).toBe("king");
|
|
163
|
+
expect(snippet?.textContent).not.toContain("&");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
111
167
|
describe("subtitle contrast (WCAG 2.2 SC 1.4.3)", () => {
|
|
112
168
|
it("renders subtitle and snippet with a contrast-safe class, not text-muted-foreground", () => {
|
|
113
169
|
const item: CmsSearchItem = {
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.9.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@salesforce/ui-bundle-template-base-sfdx-project",
|
|
9
|
-
"version": "12.
|
|
9
|
+
"version": "12.9.0",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"fast-xml-parser": "^5.9.3",
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/ui-bundle-template-app-react-sample-b2e",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.9.0",
|
|
4
4
|
"description": "Salesforce sample property rental React app",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"author": "",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"clean": "rm -rf dist"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@salesforce/ui-bundle": "^12.
|
|
19
|
+
"@salesforce/ui-bundle": "^12.9.0",
|
|
20
20
|
"sonner": "^1.7.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|