@designcrowd/library.brand-page 6.0.6-json-prereq-2 → 6.0.6-json-prereq-4
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/lib/src/{CreatedOnBrandCrowd-de-de-F1Ldbx6c.js → CreatedOnBrandCrowd-de-de-B75jYGAG.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-D8z2m0Et.js → CreatedOnBrandCrowd-en-us-CoDbU42G.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CLSkbkYu.js → CreatedOnBrandCrowd-es-es-CkQBC_E2.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cyc4YWYR.js → CreatedOnBrandCrowd-fr-ca-BueYGoqR.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cyc4YWYR.js → CreatedOnBrandCrowd-fr-fr-BueYGoqR.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-DSSUcSmV.js → CreatedOnBrandCrowd-pt-br-iVkkV5mY.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-DSSUcSmV.js → CreatedOnBrandCrowd-pt-pt-iVkkV5mY.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-DF1JaJPE.js → CreatedOnDcom-de-de-DTcMQ9rQ.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-Dek2GCu0.js → CreatedOnDcom-en-us-C6KX2pNG.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-C3JD7_bP.js → CreatedOnDcom-es-es-Bx2DoOtp.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-Bllxm84y.js → CreatedOnDcom-fr-ca-CMnpCJqe.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-B3cFZgRB.js → CreatedOnDcom-fr-fr-CIDsOdaw.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DHCnZdZk.js → CreatedOnDcom-pt-br-CbrdeuQe.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-BLooAZOd.js → CreatedOnDcom-pt-pt-nOvChX6_.js} +1 -1
- package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
- package/lib/src/brand-page/utils/view-model.util.js +68 -20
- package/lib/src/brand-page-maker/validators/field.validator.js +7 -2
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-BWEaceMZ.js → lib-BXP4Pl2X.js} +10353 -10333
- package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +14 -1
- package/package.json +1 -1
- package/src/brand-page/utils/view-model.pageid-rte.test.ts +104 -0
- package/src/brand-page/utils/view-model.util.ts +94 -26
- package/src/brand-page-maker/validators/field.validator.test.ts +50 -0
- package/src/brand-page-maker/validators/field.validator.ts +7 -2
- package/src/shared/content-settings/components/GenericTextInput.mixin.ts +17 -1
|
@@ -144,7 +144,20 @@ export default defineComponent({
|
|
|
144
144
|
},
|
|
145
145
|
},
|
|
146
146
|
watch: {
|
|
147
|
-
value(newVal) {
|
|
147
|
+
value(newVal, oldVal) {
|
|
148
|
+
// BP-5787: RTE values are Tiptap JSON objects. The parent (TextItemsField)
|
|
149
|
+
// re-clones its items array (cloneDeep) on ANY change — including a sibling
|
|
150
|
+
// field's style sync — yielding a NEW object reference with identical text
|
|
151
|
+
// content. For objects Vue fires this watcher on reference change, so that
|
|
152
|
+
// spurious re-clone would overwrite `_value` and clobber an in-flight edit
|
|
153
|
+
// (a mark applied but not yet debounced to the store), silently dropping
|
|
154
|
+
// the user's formatting. Strings never hit this (same content = same
|
|
155
|
+
// reference = no fire), so mirror that: ignore re-clones whose content is
|
|
156
|
+
// unchanged from the previous prop value.
|
|
157
|
+
if (this.showRichTextEditor &&
|
|
158
|
+
JSON.stringify(newVal) === JSON.stringify(oldVal)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
148
161
|
this._value = newVal;
|
|
149
162
|
if (this.showRichTextEditor) {
|
|
150
163
|
const textValue = escapeContentText(this._value);
|
package/package.json
CHANGED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { resolvePageIdsInRteJson } from './view-model.util';
|
|
3
|
+
import type { NavViewModel } from '../view-models';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BP-5847 — internal RTE hyperlinks are stored as `href="pageid:<id>"`. On the
|
|
7
|
+
* published site these must resolve to the page's relative path. The string
|
|
8
|
+
* (legacy HTML) path already did this; once RTE moved to Tiptap JSON the link
|
|
9
|
+
* marks were skipped, so links rendered as raw `pageid:` and didn't navigate.
|
|
10
|
+
*/
|
|
11
|
+
describe('resolvePageIdsInRteJson (BP-5847)', () => {
|
|
12
|
+
const nav: NavViewModel = {
|
|
13
|
+
navItems: [
|
|
14
|
+
{
|
|
15
|
+
pageId: 'home-id',
|
|
16
|
+
slug: 'home',
|
|
17
|
+
displayName: 'Home',
|
|
18
|
+
routeName: 'home',
|
|
19
|
+
routeParams: { slug: 'mysite' },
|
|
20
|
+
isRoot: true,
|
|
21
|
+
isHidden: false,
|
|
22
|
+
relativePath: '',
|
|
23
|
+
nestedNavItems: [],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
pageId: 'about-id',
|
|
27
|
+
slug: 'about',
|
|
28
|
+
displayName: 'About',
|
|
29
|
+
routeName: 'about',
|
|
30
|
+
routeParams: { slug: 'mysite' },
|
|
31
|
+
isRoot: false,
|
|
32
|
+
isHidden: false,
|
|
33
|
+
relativePath: 'about',
|
|
34
|
+
nestedNavItems: [],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const options = { brandPageSlug: 'mysite' } as any;
|
|
40
|
+
|
|
41
|
+
const docWithLink = (href: string) => ({
|
|
42
|
+
type: 'doc',
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'paragraph',
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: 'link',
|
|
50
|
+
marks: [{ type: 'link', attrs: { href, target: '_blank' } }],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const linkHref = (doc: any) => doc.content[0].content[0].marks[0].attrs.href;
|
|
58
|
+
|
|
59
|
+
it('resolves a pageid link to a sub-page relative path', () => {
|
|
60
|
+
const out = resolvePageIdsInRteJson(
|
|
61
|
+
docWithLink('pageid:about-id'),
|
|
62
|
+
nav,
|
|
63
|
+
options,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(linkHref(out)).toBe('/mysite/about');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('resolves a root pageid link to the site root', () => {
|
|
70
|
+
const out = resolvePageIdsInRteJson(
|
|
71
|
+
docWithLink('pageid:home-id'),
|
|
72
|
+
nav,
|
|
73
|
+
options,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(linkHref(out)).toBe('/mysite');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to root when the pageid cannot be matched', () => {
|
|
80
|
+
const out = resolvePageIdsInRteJson(
|
|
81
|
+
docWithLink('pageid:missing-id'),
|
|
82
|
+
nav,
|
|
83
|
+
options,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(linkHref(out)).toBe('/mysite');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('leaves external links untouched and returns the same reference', () => {
|
|
90
|
+
const doc = docWithLink('https://example.com');
|
|
91
|
+
const out = resolvePageIdsInRteJson(doc, nav, options);
|
|
92
|
+
|
|
93
|
+
expect(linkHref(out)).toBe('https://example.com');
|
|
94
|
+
expect(out).toBe(doc);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not mutate the source doc (returns a new object) when resolving', () => {
|
|
98
|
+
const doc = docWithLink('pageid:about-id');
|
|
99
|
+
const out = resolvePageIdsInRteJson(doc, nav, options);
|
|
100
|
+
|
|
101
|
+
expect(out).not.toBe(doc);
|
|
102
|
+
expect(linkHref(doc)).toBe('pageid:about-id');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -119,7 +119,7 @@ import {
|
|
|
119
119
|
getRouteNameByPageIndex,
|
|
120
120
|
getRouteParamsByPage,
|
|
121
121
|
} from './route.util';
|
|
122
|
-
import { hasRTEContent } from '../../brand-page-maker/utils';
|
|
122
|
+
import { hasRTEContent, isTiptapDocument } from '../../brand-page-maker/utils';
|
|
123
123
|
import {
|
|
124
124
|
getPageIdFromHref,
|
|
125
125
|
isHrefPageId,
|
|
@@ -1518,6 +1518,86 @@ const mapPageIdReferencesToRelativePaths = (
|
|
|
1518
1518
|
});
|
|
1519
1519
|
};
|
|
1520
1520
|
|
|
1521
|
+
// Resolve a `pageid:<id>` internal-link href to the published relative path.
|
|
1522
|
+
// Returns the resolved href, or null when `href` is not an internal pageid link
|
|
1523
|
+
// (external/absolute links are left untouched). Falls back to the site root when
|
|
1524
|
+
// the pageid can't be matched to a nav route.
|
|
1525
|
+
const resolvePageIdHref = (
|
|
1526
|
+
href: string,
|
|
1527
|
+
nav: NavViewModel,
|
|
1528
|
+
options: ViewModelMappingOptions,
|
|
1529
|
+
): string | null => {
|
|
1530
|
+
// Only process internal links (no protocol, doesn't start with /)
|
|
1531
|
+
if (!href || href.match(/^(https?:\/\/|\/)/) || !isHrefPageId(href)) {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const pageId = getPageIdFromHref(href);
|
|
1536
|
+
const route = findNavItemByPageId(nav.navItems, pageId);
|
|
1537
|
+
const brandPageSlugRoute = options.brandPageSlug
|
|
1538
|
+
? `/${options.brandPageSlug}`
|
|
1539
|
+
: '/';
|
|
1540
|
+
|
|
1541
|
+
if (route) {
|
|
1542
|
+
if (route.isRoot) {
|
|
1543
|
+
return brandPageSlugRoute;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
return `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// fallback to root if pageid cannot be resolved.
|
|
1550
|
+
return brandPageSlugRoute;
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
// BP-5847: walk a Tiptap JSON doc and rewrite every link mark whose href is a
|
|
1554
|
+
// `pageid:` reference into its published relative path. Returns a NEW doc when
|
|
1555
|
+
// something changed (so the shared model doc is never mutated), else the same
|
|
1556
|
+
// reference.
|
|
1557
|
+
export const resolvePageIdsInRteJson = (
|
|
1558
|
+
doc: unknown,
|
|
1559
|
+
nav: NavViewModel,
|
|
1560
|
+
options: ViewModelMappingOptions,
|
|
1561
|
+
): unknown => {
|
|
1562
|
+
let changed = false;
|
|
1563
|
+
|
|
1564
|
+
const walk = (node: any): any => {
|
|
1565
|
+
if (!node || typeof node !== 'object') {
|
|
1566
|
+
return node;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
let next = node;
|
|
1570
|
+
|
|
1571
|
+
if (Array.isArray(node.marks)) {
|
|
1572
|
+
const marks = node.marks.map((mark: any) => {
|
|
1573
|
+
if (mark?.type === 'link' && typeof mark.attrs?.href === 'string') {
|
|
1574
|
+
const resolved = resolvePageIdHref(mark.attrs.href, nav, options);
|
|
1575
|
+
|
|
1576
|
+
if (resolved !== null) {
|
|
1577
|
+
changed = true;
|
|
1578
|
+
|
|
1579
|
+
return { ...mark, attrs: { ...mark.attrs, href: resolved } };
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return mark;
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
next = { ...next, marks };
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (Array.isArray(node.content)) {
|
|
1590
|
+
next = { ...next, content: node.content.map(walk) };
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return next;
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
const result = walk(doc);
|
|
1597
|
+
|
|
1598
|
+
return changed ? result : doc;
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1521
1601
|
const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
|
|
1522
1602
|
content: any,
|
|
1523
1603
|
nav: NavViewModel,
|
|
@@ -1530,7 +1610,15 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
|
|
|
1530
1610
|
return;
|
|
1531
1611
|
}
|
|
1532
1612
|
|
|
1533
|
-
if (
|
|
1613
|
+
if (isTiptapDocument(value)) {
|
|
1614
|
+
// BP-5847: RTE fields may now be Tiptap JSON. Resolve pageid link marks
|
|
1615
|
+
// here too, or internal links render as raw `pageid:` and don't navigate.
|
|
1616
|
+
(content as unknown as Record<string, unknown>)[key] =
|
|
1617
|
+
resolvePageIdsInRteJson(value, nav, options);
|
|
1618
|
+
} else if (
|
|
1619
|
+
typeof value === 'object' &&
|
|
1620
|
+
(key === 'items' || key === 'textItems')
|
|
1621
|
+
) {
|
|
1534
1622
|
if (Array.isArray(value)) {
|
|
1535
1623
|
value.forEach((item) => {
|
|
1536
1624
|
dynamicallyMapPageIdInHyperlinksToRelativePaths(item, nav, options);
|
|
@@ -1542,31 +1630,11 @@ const dynamicallyMapPageIdInHyperlinksToRelativePaths = (
|
|
|
1542
1630
|
(content as unknown as Record<string, unknown>)[key] = value.replace(
|
|
1543
1631
|
/<a\s+([^>]*?)href="([^"]*?)"([^>]*?)>/gi,
|
|
1544
1632
|
(match, existingAttrsStart, href, existingAttrsEnd) => {
|
|
1545
|
-
|
|
1546
|
-
if (href && !href.match(/^(https?:\/\/|\/)/) && isHrefPageId(href)) {
|
|
1547
|
-
const pageId = getPageIdFromHref(href);
|
|
1548
|
-
|
|
1549
|
-
const route = findNavItemByPageId(nav.navItems, pageId);
|
|
1550
|
-
|
|
1551
|
-
const brandPageSlugRoute = options.brandPageSlug
|
|
1552
|
-
? `/${options.brandPageSlug}`
|
|
1553
|
-
: '/';
|
|
1554
|
-
|
|
1555
|
-
if (route) {
|
|
1556
|
-
let href = `${brandPageSlugRoute}`;
|
|
1557
|
-
|
|
1558
|
-
if (!route.isRoot) {
|
|
1559
|
-
href = `${brandPageSlugRoute}${brandPageSlugRoute === '/' ? '' : '/'}${route.relativePath}`;
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
return `<a ${existingAttrsStart}href="${href}"${existingAttrsEnd}>`;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
// fallback to root if pageid cannot be resolved.
|
|
1566
|
-
return `<a ${existingAttrsStart}href="${brandPageSlugRoute}"${existingAttrsEnd}>`;
|
|
1567
|
-
}
|
|
1633
|
+
const resolved = resolvePageIdHref(href, nav, options);
|
|
1568
1634
|
|
|
1569
|
-
return
|
|
1635
|
+
return resolved === null
|
|
1636
|
+
? match
|
|
1637
|
+
: `<a ${existingAttrsStart}href="${resolved}"${existingAttrsEnd}>`;
|
|
1570
1638
|
},
|
|
1571
1639
|
);
|
|
1572
1640
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validatorDict } from './field.validator';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* BP-5857 — the `title` field validator must accept RTE content in both forms.
|
|
6
|
+
* After BP-5787, switchAllContentToRTE converts section titles (Payments, Store,
|
|
7
|
+
* Tipping, Social Links) from a plain string to a Tiptap JSON doc. The old
|
|
8
|
+
* `typeof value === 'string'` check then flagged a title that clearly HAS text
|
|
9
|
+
* as empty ("Please enter a value"). The validator now dual-reads the value.
|
|
10
|
+
*/
|
|
11
|
+
describe('field.validator — title (nonEmptyString)', () => {
|
|
12
|
+
const titleValidator = validatorDict.title;
|
|
13
|
+
|
|
14
|
+
const jsonDoc = (text: string) => ({
|
|
15
|
+
type: 'doc',
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: 'paragraph',
|
|
19
|
+
content: text
|
|
20
|
+
? [{ type: 'text', text, marks: [{ type: 'bold' }] }]
|
|
21
|
+
: [],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('accepts a non-empty legacy string', () => {
|
|
27
|
+
expect(titleValidator.validate('title', 'Payments').isValid).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects an empty string', () => {
|
|
31
|
+
expect(titleValidator.validate('title', '').isValid).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('accepts a Tiptap JSON doc that has visible text (BP-5857)', () => {
|
|
35
|
+
expect(titleValidator.validate('title', jsonDoc('Payments')).isValid).toBe(
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rejects an empty Tiptap JSON doc', () => {
|
|
41
|
+
expect(titleValidator.validate('title', jsonDoc('')).isValid).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects null/undefined without throwing', () => {
|
|
45
|
+
expect(titleValidator.validate('title', null as any).isValid).toBe(false);
|
|
46
|
+
expect(titleValidator.validate('title', undefined as any).isValid).toBe(
|
|
47
|
+
false,
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isURL, isEmail, isTel } from '@utils/validator.util';
|
|
2
2
|
import type { ItemFieldType, Validator } from '@brand-page-maker/models';
|
|
3
|
-
import { YOUTUBE_URL_RE } from '@brand-page-maker/utils';
|
|
3
|
+
import { YOUTUBE_URL_RE, getPlainTextFromRTE } from '@brand-page-maker/utils';
|
|
4
4
|
import { validatorTr } from '@src/composables/useTranslate';
|
|
5
5
|
|
|
6
6
|
const addressValueValidator: Validator = {
|
|
@@ -76,7 +76,12 @@ export const dummyValidator: Validator = {
|
|
|
76
76
|
|
|
77
77
|
const nonEmptyStringValidator: Validator = {
|
|
78
78
|
validate: (name, value: any) => {
|
|
79
|
-
|
|
79
|
+
// BP-5787/BP-5857: `title` fields may now hold Tiptap JSON (an RTE doc)
|
|
80
|
+
// rather than a plain string once switchAllContentToRTE converts them.
|
|
81
|
+
// Validate the visible text either way — a doc with text is "entered",
|
|
82
|
+
// even though `typeof value` is 'object'. getPlainTextFromRTE dual-reads
|
|
83
|
+
// both forms (string → stripped text, JSON → concatenated text).
|
|
84
|
+
const isValid = getPlainTextFromRTE(value).trim().length > 0;
|
|
80
85
|
|
|
81
86
|
return {
|
|
82
87
|
isValid,
|
|
@@ -159,7 +159,23 @@ export default defineComponent({
|
|
|
159
159
|
},
|
|
160
160
|
},
|
|
161
161
|
watch: {
|
|
162
|
-
value(newVal: string | undefined) {
|
|
162
|
+
value(newVal: string | undefined, oldVal: string | undefined) {
|
|
163
|
+
// BP-5787: RTE values are Tiptap JSON objects. The parent (TextItemsField)
|
|
164
|
+
// re-clones its items array (cloneDeep) on ANY change — including a sibling
|
|
165
|
+
// field's style sync — yielding a NEW object reference with identical text
|
|
166
|
+
// content. For objects Vue fires this watcher on reference change, so that
|
|
167
|
+
// spurious re-clone would overwrite `_value` and clobber an in-flight edit
|
|
168
|
+
// (a mark applied but not yet debounced to the store), silently dropping
|
|
169
|
+
// the user's formatting. Strings never hit this (same content = same
|
|
170
|
+
// reference = no fire), so mirror that: ignore re-clones whose content is
|
|
171
|
+
// unchanged from the previous prop value.
|
|
172
|
+
if (
|
|
173
|
+
this.showRichTextEditor &&
|
|
174
|
+
JSON.stringify(newVal) === JSON.stringify(oldVal)
|
|
175
|
+
) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
this._value = newVal;
|
|
164
180
|
|
|
165
181
|
if (this.showRichTextEditor) {
|