Skip to content

OBPIH-6272 Add source code with hyperlink to source save success message#5673

Merged
alannadolny merged 4 commits into
developfrom
ft/OBPIH-6272
Dec 16, 2025
Merged

OBPIH-6272 Add source code with hyperlink to source save success message#5673
alannadolny merged 4 commits into
developfrom
ft/OBPIH-6272

Conversation

@alannadolny
Copy link
Copy Markdown
Collaborator

No description provided.

@alannadolny alannadolny self-assigned this Dec 15, 2025
@github-actions github-actions Bot added type: feature A new piece of functionality for the app domain: frontend Changes or discussions relating to the frontend UI domain: l10n Changes or discussions relating to localization & Internationalization labels Dec 15, 2025
@codecov
Copy link
Copy Markdown

codecov Bot commented Dec 15, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 9.70%. Comparing base (1bb7314) to head (45822f0).
⚠️ Report is 291 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff              @@
##             develop   #5673      +/-   ##
============================================
+ Coverage       9.12%   9.70%   +0.58%     
- Complexity      1170    1322     +152     
============================================
  Files            701     717      +16     
  Lines          45281   45803     +522     
  Branches       10851   10930      +79     
============================================
+ Hits            4131    4445     +314     
- Misses         40497   40670     +173     
- Partials         653     688      +35     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a clickable hyperlink to the product supplier code in success messages when creating or updating a product source. It introduces a new custom hook useTranslateWithRedirect that enables embedding links within translated text by replacing specified phrases with HTML anchor tags. The product supplier form is updated to use this new hook, and the i18n messages are modified to include the product code as a dynamic placeholder.

Key Changes

  • Created useTranslateWithRedirect hook to support translations with embedded hyperlinks
  • Updated product supplier form success messages to include clickable product code links
  • Modified i18n message templates to interpolate the product code

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/js/hooks/useTranslateWithRedirect.jsx New hook that extends translation functionality with link replacement capabilities using dangerouslySetInnerHTML
src/js/hooks/productSupplier/form/useProductSupplierForm.js Updated to use useTranslateWithRedirect for success messages, extracting product code from API response and creating clickable links
grails-app/i18n/messages.properties Updated success message templates to include ${code} placeholder for product code interpolation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
const anchorTag = `<a href="${redirectTo}" target="_blank">${phrase}</a>`;
return acc.replace(phrase, anchorTag);
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The String.replace() method only replaces the first occurrence of the phrase. If the same phrase appears multiple times in the translated text, only the first occurrence will be replaced with a link, leaving subsequent occurrences as plain text. This can lead to inconsistent behavior and confusing user experience.

Use String.replaceAll() instead, or use a regular expression with the global flag to replace all occurrences.

Suggested change
return acc.replace(phrase, anchorTag);
return acc.replaceAll(phrase, anchorTag);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it can lead to too many changes in the string. If someone wants to replace more than one occurrence, they can do it by passing that argument more than once. Plus, sometimes the same phase can have different redirects, like the string "Read more."

Comment on lines +341 to +349
const successMessage = translateWithRedirect({
label: `react.productSupplier.form.success.${productSupplierId ? 'update' : 'create'}`,
defaultLabel: `Product ${productSupplierCode} has been ${productSupplierId ? 'updated' : 'created'} successfully`,
options: { code: productSupplierCode },
redirects: [{
phrase: productSupplierCode,
redirectTo: PRODUCT_SUPPLIER_URL.edit(productSupplier),
}],
});
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The productSupplierCode may be undefined if the API response structure is unexpected or if the code field is missing. Using an undefined value in the phrase parameter will cause the error check on line 43 to throw an error with "The phrase 'undefined' was not found in the translation", and even if it somehow passes, it will create a link with literal text "undefined" which provides a poor user experience.

Add a null/undefined check for productSupplierCode before using it, and provide a fallback behavior when the code is not available.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎

});

const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
const anchorTag = `<a href="${redirectTo}" target="_blank">${phrase}</a>`;
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The anchor tag is missing the rel="noopener noreferrer" attribute which is a security best practice when using target="_blank". Without these attributes, the opened page can access the window.opener property and potentially redirect the parent page to a malicious URL (tabnabbing attack).

Add rel="noopener noreferrer" to the anchor tag for security.

Suggested change
const anchorTag = `<a href="${redirectTo}" target="_blank">${phrase}</a>`;
const anchorTag = `<a href="${redirectTo}" target="_blank" rel="noopener noreferrer">${phrase}</a>`;

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +48
redirects.forEach(({ phrase }) => {
if (!content.includes(phrase)) {
throw new Error(`The phrase "${phrase}" was not found in the translation`);
}
});

const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing an error during render is a poor user experience and will crash the component. If a phrase is not found in the translation (perhaps due to a translation update or configuration error), the entire UI will break with a white screen.

Consider handling this case more gracefully by either logging a warning and returning the original translated text without the link, or rendering an error message to the user while keeping the UI functional.

Suggested change
redirects.forEach(({ phrase }) => {
if (!content.includes(phrase)) {
throw new Error(`The phrase "${phrase}" was not found in the translation`);
}
});
const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
// For each redirect, check if the phrase exists; warn if not, but do not throw.
redirects.forEach(({ phrase }) => {
if (!content.includes(phrase)) {
console.warn(`useTranslateWithRedirect: The phrase "${phrase}" was not found in the translation for label "${label}". Skipping link replacement for this phrase.`);
}
});
const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
if (!acc.includes(phrase)) {
// If the phrase is not found, skip replacement for this phrase.
return acc;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should crash the component, because it means that the developer misuses that hook.

}
});

const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If multiple redirect configurations have overlapping phrases or one phrase is a substring of another, the replacement order may cause unexpected results. For example, if one phrase is "Product ABC" and another is "ABC", the first replacement could inadvertently prevent the second from matching, or create malformed HTML.

Consider validating that phrases don't overlap or contain each other, or process longer phrases first to avoid this issue.

Suggested change
const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
// Sort redirects by phrase length descending to avoid substring replacement issues
const sortedRedirects = [...redirects].sort((a, b) => b.phrase.length - a.phrase.length);
const stringifiedHTML = sortedRedirects.reduce((acc, { phrase, redirectTo }) => {

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stop overthinking

Comment thread src/js/hooks/useTranslateWithRedirect.jsx Outdated
Comment on lines +48 to +54
const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
const anchorTag = `<a href="${redirectTo}" target="_blank">${phrase}</a>`;
return acc.replace(phrase, anchorTag);
}, content);

return (
<span dangerouslySetInnerHTML={{ __html: stringifiedHTML }} />
Copy link

Copilot AI Dec 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of dangerouslySetInnerHTML with unsanitized user input creates a Cross-Site Scripting (XSS) vulnerability. The phrase and redirectTo parameters are directly interpolated into HTML without any sanitization or escaping. If either of these values contains malicious HTML/JavaScript code, it will be executed in the user's browser.

Consider using a dedicated HTML sanitization library (like DOMPurify) to sanitize the HTML before rendering, or better yet, avoid dangerouslySetInnerHTML entirely by constructing React elements programmatically.

Suggested change
const stringifiedHTML = redirects.reduce((acc, { phrase, redirectTo }) => {
const anchorTag = `<a href="${redirectTo}" target="_blank">${phrase}</a>`;
return acc.replace(phrase, anchorTag);
}, content);
return (
<span dangerouslySetInnerHTML={{ __html: stringifiedHTML }} />
// Replace each phrase with a React <a> element, preserving order and avoiding XSS
let nodes = [content];
redirects.forEach(({ phrase, redirectTo }, idx) => {
const newNodes = [];
nodes.forEach((node, nodeIdx) => {
if (typeof node === 'string') {
const parts = node.split(phrase);
for (let i = 0; i < parts.length; i++) {
if (parts[i]) newNodes.push(parts[i]);
if (i < parts.length - 1) {
newNodes.push(
<a
key={`redirect-link-${idx}-${nodeIdx}-${i}`}
href={redirectTo}
target="_blank"
rel="noopener noreferrer"
>
{phrase}
</a>
);
}
}
} else {
newNodes.push(node);
}
});
nodes = newNodes;
});
return (
<span>
{nodes}
</span>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤮

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this suggested code is overly complex.

So I suppose the reason you use dangerouslySetInnerHTML is because

  1. this isn't user input so we don't need to check for XSS
  2. it's simpler code

you could build it somewhat similarly to copilot such that it doesn't require using dangerouslySetInnerHTML (which has a scary name) but I don't know then frontend well enough to know the benefits so I'll defer to you.

alannadolny and others added 2 commits December 15, 2025 15:21
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@alannadolny alannadolny merged commit 1386668 into develop Dec 16, 2025
7 checks passed
@alannadolny alannadolny deleted the ft/OBPIH-6272 branch December 16, 2025 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: frontend Changes or discussions relating to the frontend UI domain: l10n Changes or discussions relating to localization & Internationalization type: feature A new piece of functionality for the app

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants