@capillarytech/creatives-library 9.0.67 → 9.0.69

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.67",
4
+ "version": "9.0.69",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -18,6 +18,7 @@ import { ANDROID, GENERIC, IOS } from '../../v2Containers/CreativesContainer/con
18
18
  import { ERROR_TAB_KEYS } from '../HtmlEditor/utils/validationConstants';
19
19
  import { SEVERITY } from '../HtmlEditor/components/ValidationPanel/constants';
20
20
  import { LIQUID_DOC_URL } from './constants';
21
+ import renderIssueMessage from '../HtmlEditor/utils/renderIssueMessage';
21
22
 
22
23
  const { CapLabelInline } = CapLabel;
23
24
 
@@ -120,7 +121,7 @@ const TabContent = ({ issues, onErrorClick, intl }) => {
120
121
  >
121
122
  <CapRow useLegacy className="error-info-note__item-content">
122
123
  <CapLabelInline type="label2" className="error-info-note__item-message">
123
- {displayMessage}
124
+ {renderIssueMessage(displayMessage)}
124
125
  </CapLabelInline>
125
126
  {line && (
126
127
  <CapLabelInline type="label2" className="error-info-note__item-location">
@@ -426,7 +427,7 @@ const ErrorSection = ({
426
427
  {(errors || []).map((error) => (
427
428
  <CapRow useLegacy key={`${error}`} className="error-list-legacy__item">
428
429
  <CapLabel type="label2" className="cap-list-v2-error-item">
429
- <CapLabel type="label2">{error}</CapLabel>
430
+ <CapLabel type="label2">{renderIssueMessage(error)}</CapLabel>
430
431
  </CapLabel>
431
432
  </CapRow>
432
433
  ))}
@@ -18,6 +18,7 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
18
18
  // Messages
19
19
  import messages from './messages';
20
20
  import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../../constants';
21
+ import renderIssueMessage from '../../utils/renderIssueMessage';
21
22
 
22
23
  // Styles
23
24
  import './_validationTabs.scss';
@@ -109,7 +110,7 @@ const ValidationTabContent = ({
109
110
  >
110
111
  <div className="validation-tabs__item-content">
111
112
  <span className="validation-tabs__item-message">
112
- {message}
113
+ {renderIssueMessage(message)}
113
114
  </span>
114
115
  {line && (
115
116
  <span className="validation-tabs__item-location">
@@ -0,0 +1,74 @@
1
+ /**
2
+ * renderIssueMessage Tests
3
+ *
4
+ * Tests for rendering AI-generated liquid validation messages, including tolerance for
5
+ * non-guaranteed LLM formatting quirks (lead-in text before the first label, markdown-wrapped
6
+ * labels).
7
+ */
8
+
9
+ import React from 'react';
10
+ import { render } from '@testing-library/react';
11
+ import '@testing-library/jest-dom';
12
+ import renderIssueMessage from '../renderIssueMessage';
13
+
14
+ const renderMessage = (message) => render(<div>{renderIssueMessage(message)}</div>);
15
+
16
+ describe('renderIssueMessage', () => {
17
+ it('returns non-string input unchanged', () => {
18
+ const node = <span>already a node</span>;
19
+ expect(renderIssueMessage(node)).toBe(node);
20
+ expect(renderIssueMessage(null)).toBeNull();
21
+ expect(renderIssueMessage(undefined)).toBeUndefined();
22
+ });
23
+
24
+ it('returns a plain message unchanged when no known labels are present', () => {
25
+ expect(renderIssueMessage('Something went wrong')).toBe('Something went wrong');
26
+ });
27
+
28
+ it('renders all three labels in order, bolding only "Issue text:" and stripping it from the body', () => {
29
+ const message = 'Issue text: bad tag Explanation: the tag is malformed Solution: fix the tag';
30
+ const { container } = renderMessage(message);
31
+
32
+ const strong = container.querySelector('strong');
33
+ expect(strong).toHaveTextContent('Issue text:');
34
+ expect(container).toHaveTextContent('Issue text: bad tag');
35
+ expect(container).toHaveTextContent('Explanation: the tag is malformed');
36
+ expect(container).toHaveTextContent('Solution: fix the tag');
37
+ // Only the "Issue text:" label should be wrapped in <strong>.
38
+ expect(container.querySelectorAll('strong')).toHaveLength(1);
39
+ });
40
+
41
+ it('renders correctly when only some labels are present', () => {
42
+ const message = 'Issue text: bad tag Solution: fix the tag';
43
+ const { container } = renderMessage(message);
44
+
45
+ expect(container).toHaveTextContent('Issue text: bad tag');
46
+ expect(container).toHaveTextContent('Solution: fix the tag');
47
+ expect(container).not.toHaveTextContent('Explanation:');
48
+ });
49
+
50
+ it('preserves lead-in text before the first recognized label instead of dropping it', () => {
51
+ const message = "Here's what I found. Issue text: bad tag Explanation: the tag is malformed";
52
+ const { container } = renderMessage(message);
53
+
54
+ expect(container).toHaveTextContent("Here's what I found.");
55
+ expect(container).toHaveTextContent('Issue text: bad tag');
56
+ });
57
+
58
+ it('strips markdown bold wrapping around a label instead of leaking asterisks', () => {
59
+ const message = '**Issue text:** bad tag **Explanation:** the tag is malformed';
60
+ const { container } = renderMessage(message);
61
+
62
+ expect(container.textContent).not.toContain('*');
63
+ expect(container.querySelector('strong')).toHaveTextContent('Issue text:');
64
+ expect(container).toHaveTextContent('Explanation: the tag is malformed');
65
+ });
66
+
67
+ it('strips markdown underscore-bold wrapping around a label', () => {
68
+ const message = '__Issue text:__ bad tag';
69
+ const { container } = renderMessage(message);
70
+
71
+ expect(container.textContent).not.toContain('_');
72
+ expect(container.querySelector('strong')).toHaveTextContent('Issue text:');
73
+ });
74
+ });
@@ -0,0 +1,56 @@
1
+ import React, { Fragment } from 'react';
2
+ import {
3
+ AI_ISSUE_TEXT_LABEL,
4
+ AI_ISSUE_MESSAGE_LABELS,
5
+ AI_ISSUE_MESSAGE_LEADING_SEGMENT_KEY,
6
+ } from './validationConstants';
7
+
8
+ // Renders askAiraForLiquid's LLM-generated messages, splitting on known labels rather than "\n"
9
+ // since the API doesn't guarantee newlines; tolerates lead-in text and markdown-wrapped labels.
10
+
11
+ const stripLabelMarkdown = (text) => AI_ISSUE_MESSAGE_LABELS.reduce(
12
+ (acc, label) => acc.split(`**${label}**`).join(label).split(`__${label}__`).join(label),
13
+ text
14
+ );
15
+
16
+ const renderIssueMessage = (message) => {
17
+ if (typeof message !== 'string') {
18
+ return message;
19
+ }
20
+
21
+ const normalizedMessage = stripLabelMarkdown(message);
22
+
23
+ const labelPositions = AI_ISSUE_MESSAGE_LABELS
24
+ .map((label) => ({ label, index: normalizedMessage.indexOf(label) }))
25
+ .filter(({ index }) => index !== -1)
26
+ .sort((a, b) => a.index - b.index);
27
+
28
+ if (labelPositions.length === 0) {
29
+ return normalizedMessage;
30
+ }
31
+
32
+ const segments = labelPositions.map(({ label, index }, i) => {
33
+ const end = i + 1 < labelPositions.length ? labelPositions[i + 1].index : normalizedMessage.length;
34
+ return { key: label, label, text: normalizedMessage.slice(index, end).trim() };
35
+ });
36
+
37
+ const leadingText = normalizedMessage.slice(0, labelPositions[0].index).trim();
38
+ if (leadingText) {
39
+ segments.unshift({ key: AI_ISSUE_MESSAGE_LEADING_SEGMENT_KEY, label: null, text: leadingText });
40
+ }
41
+
42
+ return segments.map(({ key, label, text }, index) => (
43
+ <Fragment key={key}>
44
+ {index > 0 && <br />}
45
+ {index > 0 && ' - '}
46
+ {label === AI_ISSUE_TEXT_LABEL ? (
47
+ <>
48
+ <strong>{label}</strong>
49
+ {text.slice(label.length)}
50
+ </>
51
+ ) : text}
52
+ </Fragment>
53
+ ));
54
+ };
55
+
56
+ export default renderIssueMessage;
@@ -36,3 +36,8 @@ export const ERROR_TAB_KEYS = {
36
36
  ERRORS: 'errors',
37
37
  WARNINGS: 'warnings',
38
38
  };
39
+
40
+ // Labels used to segment AI-generated liquid validation messages (see renderIssueMessage.js)
41
+ export const AI_ISSUE_TEXT_LABEL = 'Issue text:';
42
+ export const AI_ISSUE_MESSAGE_LABELS = [AI_ISSUE_TEXT_LABEL, 'Explanation:', 'Solution:'];
43
+ export const AI_ISSUE_MESSAGE_LEADING_SEGMENT_KEY = 'leading-text';
@@ -28,11 +28,11 @@ export default defineMessages({
28
28
  },
29
29
  "exportHtmlDisabledCreateTooltip": {
30
30
  id: 'creatives.containersV2.Email.exportHtmlDisabledCreateTooltip',
31
- defaultMessage: "HTML export isn’t available while creating a template.",
31
+ defaultMessage: "Your template is empty. Add content, then save it to export as HTML.",
32
32
  },
33
33
  "exportHtmlDisabledEditedTooltip": {
34
34
  id: 'creatives.containersV2.Email.exportHtmlDisabledEditedTooltip',
35
- defaultMessage: "HTML export is unavailable because the template has been edited in this session.",
35
+ defaultMessage: "You have unsaved edits. Save the template to export the latest HTML.",
36
36
  },
37
37
  "alphabeticallyOption": {
38
38
  id: 'creatives.containersV2.Email.alphabeticallyOption',