@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.
Files changed (26) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-F1Ldbx6c.js → CreatedOnBrandCrowd-de-de-B75jYGAG.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-D8z2m0Et.js → CreatedOnBrandCrowd-en-us-CoDbU42G.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CLSkbkYu.js → CreatedOnBrandCrowd-es-es-CkQBC_E2.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-Cyc4YWYR.js → CreatedOnBrandCrowd-fr-ca-BueYGoqR.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-Cyc4YWYR.js → CreatedOnBrandCrowd-fr-fr-BueYGoqR.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-DSSUcSmV.js → CreatedOnBrandCrowd-pt-br-iVkkV5mY.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-DSSUcSmV.js → CreatedOnBrandCrowd-pt-pt-iVkkV5mY.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-DF1JaJPE.js → CreatedOnDcom-de-de-DTcMQ9rQ.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Dek2GCu0.js → CreatedOnDcom-en-us-C6KX2pNG.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-C3JD7_bP.js → CreatedOnDcom-es-es-Bx2DoOtp.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-Bllxm84y.js → CreatedOnDcom-fr-ca-CMnpCJqe.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-B3cFZgRB.js → CreatedOnDcom-fr-fr-CIDsOdaw.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-DHCnZdZk.js → CreatedOnDcom-pt-br-CbrdeuQe.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BLooAZOd.js → CreatedOnDcom-pt-pt-nOvChX6_.js} +1 -1
  15. package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
  16. package/lib/src/brand-page/utils/view-model.util.js +68 -20
  17. package/lib/src/brand-page-maker/validators/field.validator.js +7 -2
  18. package/lib/src/index.mjs +1 -1
  19. package/lib/src/{lib-BWEaceMZ.js → lib-BXP4Pl2X.js} +10353 -10333
  20. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +14 -1
  21. package/package.json +1 -1
  22. package/src/brand-page/utils/view-model.pageid-rte.test.ts +104 -0
  23. package/src/brand-page/utils/view-model.util.ts +94 -26
  24. package/src/brand-page-maker/validators/field.validator.test.ts +50 -0
  25. package/src/brand-page-maker/validators/field.validator.ts +7 -2
  26. 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.6-json-prereq-2",
4
+ "version": "6.0.6-json-prereq-4",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -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 (typeof value === 'object' && (key === 'items' || key === 'textItems')) {
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
- // Only process internal links (no protocol, doesn't start with /)
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 match;
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
- const isValid = typeof value === 'string' && value.length > 0;
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) {