Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/console/.env.local
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# ----------------------------------------------------------------------------------
# Dev Server Configs
# ----------------------------------------------------------------------------------
DEV_SERVER_PORT=9001
# change the dev server port to avoid to conflict
Comment thread
pavinduLakshan marked this conversation as resolved.
Outdated
DEV_SERVER_PORT=9002
DEV_SERVER_HOST=localhost
DISABLE_DEV_SERVER_HOST_CHECK=false
DISABLE_ESLINT_PLUGIN=false
Expand All @@ -11,7 +12,7 @@ ENABLE_ANALYZER=false
ANALYZER_PORT=8889
LOG_LEVEL=none

WDS_SOCKET_PORT=9001
WDS_SOCKET_PORT=9002
WDS_SOCKET_HOST=localhost

# ----------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { getSelectedNode } from "../utils/get-selected-node";
import "./link-plugin.scss";
import FormControlLabel from "@oxygen-ui/react/FormControlLabel/FormControlLabel";
import Checkbox from "@oxygen-ui/react/Checkbox";
import Hint from "../../../resources/elements/hint";
import Box from "@oxygen-ui/react/Box/Box";
import Tooltip from "@oxygen-ui/react/Tooltip";

const LowPriority: CommandListenerPriority = 1;
const HighPriority: CommandListenerPriority = 3;
Expand Down Expand Up @@ -162,7 +167,7 @@ const LinkEditor = (): ReactElement => {
const [ isEditMode, setEditMode ] = useState(false);
const [ lastSelection, setLastSelection ] = useState<BaseSelection | null>(null);
const [ selectedUrlType, setSelectedUrlType ] = useState<string>("CUSTOM");

const [ linkTarget, setLinkTarget ] = useState<"_blank" | "_self">("_blank");

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.

it seems "_blank" | "_self" is repeated throughout the file. Shall we extract it to a reusable type definition?

const { t } = useTranslation();

/**
Expand All @@ -178,14 +183,18 @@ const LinkEditor = (): ReactElement => {

if ($isLinkNode(parent)) {
const url: string = parent.getURL();
const target:string = parent.getTarget() || "_blank";

setLinkUrl(getPlaceholderUrl(url));
setSelectedUrlType(determineUrlType(url));
setLinkTarget(target as "_blank" | "_self");
} else if ($isLinkNode(node)) {
const url: string = node.getURL();
const target:string = node.getTarget() || "_blank";

setLinkUrl(getPlaceholderUrl(url));
setSelectedUrlType(determineUrlType(url));
setLinkTarget(target as "_blank" | "_self");
} else {
setLinkUrl("");
setSelectedUrlType("CUSTOM");
Expand Down Expand Up @@ -367,20 +376,24 @@ const LinkEditor = (): ReactElement => {
if (url) {
// First use the default command to handle the link creation/update.
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);

// Then update the link attributes to include safe properties.
const selection: BaseSelection = $getSelection();
const selection: BaseSelection | null = $getSelection();

if ($isRangeSelection(selection)) {
const node: TextNode | ElementNode = getSelectedNode(selection);
const linkNode: ElementNode = $isLinkNode(node) ? node : node.getParent();

if ($isLinkNode(linkNode)) {
// Update the link node with safe attributes.
linkNode.setTarget("_blank");
linkNode.setRel("noopener noreferrer");
linkNode.setTarget(linkTarget);
if(linkTarget === "_blank") {
linkNode.setRel("noopener noreferrer");
} else {
linkNode.setRel("");
}
Comment on lines +394 to +396

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.

Is this else block necessary?

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.

@Mahima-Sanketh-Git shall we address this as well? I believe this else block is necessary because rel is already empty if the linkTarget === "_blank" condition is not met.

@coderabbitai please validate.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@pavinduLakshan — the else block is actually necessary. Here's why pavinduLakshan's assumption doesn't hold:

The problematic scenario:

  1. User creates a link with "Open in new tab" checked → Lexical link node gets target="_blank" and rel="noopener noreferrer".
  2. User later edits the link and unchecks "Open in new tab" (linkTarget = "_self").
  3. On save, TOGGLE_SAFE_LINK_COMMAND is dispatched.
  4. Inside the handler, TOGGLE_LINK_COMMAND is dispatched first — this updates the URL on the existing link node but preserves other attributes, including the previously set rel="noopener noreferrer".
  5. Without the else block, the handler would skip the setRel call, leaving rel="noopener noreferrer" on a _self link.

So rel is not guaranteed to be empty — it retains whatever value was set during a prior _blank save. The else block explicitly clears it, which is the correct behavior.

The else block should be kept as-is. ✅


🧠 Learnings used
Learnt from: PasinduYeshan
Repo: wso2/identity-apps PR: 9589
File: features/admin.connections.v1/components/edit/settings/outbound-provisioning-settings.tsx:575-578
Timestamp: 2026-02-15T17:06:33.975Z
Learning: In the identity-apps repository, prefer using the data-componentid attribute for component identification in tests. The data-testid attribute is deprecated. Use data-componentid for new components, while existing components may continue using data-testid until they are updated.

Learnt from: ShanChathusanda93
Repo: wso2/identity-apps PR: 9619
File: features/admin.issuer-usage-scope.v1/pages/issuer-usage-scope-configuration.tsx:74-76
Timestamp: 2026-02-18T10:58:59.745Z
Learning: In WSO2 Identity Apps, for any feature that modifies server configurations via the /api/server/v1/configs/ endpoint (e.g., Issuer Usage Scope), perform permission checks using featureConfig?.server?.scopes?.update instead of creating dedicated scope configurations. Apply this guideline to all relevant TSX files under features that interact with server config endpoints.

Learnt from: VivekVinushanth
Repo: wso2/identity-apps PR: 9651
File: features/admin.cds.v1/components/profile-attribute.tsx:25-27
Timestamp: 2026-02-25T11:23:23.686Z
Learning: In TSX files across the repo, when implementing dynamic key-value inputs and there is no equivalent component in wso2is/form (e.g., dynamic values editor), it is acceptable to reuse the legacy DynamicField as a temporary solution until an Oxygen UI alternative is built. Avoid creating a custom react-final-form FieldArray unless absolutely necessary. This guidance applies to components like profile-attribute.tsx and similar dynamic-field scenarios.

Learnt from: Lakshan-Banneheke
Repo: wso2/identity-apps PR: 10215
File: features/admin.groups.v1/components/wizard/create-group-wizard.tsx:603-611
Timestamp: 2026-04-29T15:34:05.529Z
Learning: When reviewing code in wso2/identity-apps, do not flag `TierLimitReachErrorModal` usages of `actionLabel` and `header` as “unused” when these props are currently not consumed by the call site. Keep the props passed through for consistency with other call sites and to support future extensibility (the component is imported from `wso2is/admin.core.v1/components/modals`).

Learnt from: hwupathum
Repo: wso2/identity-apps PR: 10375
File: features/admin.consents.v1/components/preference-management-list.tsx:40-40
Timestamp: 2026-06-08T07:41:36.308Z
Learning: In this repo’s feature UI components, when rendering small badge/pill-style labels (e.g., “Shared policy”, “Default”) in list-like UIs, use Oxygen UI’s `Chip` component from `oxygen-ui/react/Chip` with `size="small"` instead of `Label` from `semantic-ui-react`. Keep using `SemanticICONS` from `semantic-ui-react` where it’s needed for table action icon/type definitions.

}
}

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.

Suggested change

} else {
// If no URL, remove the link (same as TOGGLE_LINK_COMMAND with null).
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
Expand All @@ -391,7 +404,7 @@ const LinkEditor = (): ReactElement => {
HighPriority
)
);
}, [ editor, updateLinkEditor, isEditMode ]);
}, [ editor, updateLinkEditor, isEditMode, linkTarget ]);

/**
* Updates the link editor position.
Expand Down Expand Up @@ -465,13 +478,66 @@ const LinkEditor = (): ReactElement => {
}
} }
/>
{/* Link Target Checkbox - With Description */}
<Box sx={ { alignItems:"center", display: "flex", flexDirection: "row", gap: 0
} }>
Comment thread
Mahima-Sanketh-Git marked this conversation as resolved.
Outdated
<FormControlLabel
control={
<Checkbox
checked={ linkTarget === "_blank" }
onChange={ (event: React.ChangeEvent<HTMLInputElement>) => {
const newTarget:"_blank" | "_self" =
event.target.checked ? "_blank" : "_self";

setLinkTarget(newTarget);

if (lastSelection !== null) {
const currentUrl:any = getCurrentUrl();

if (currentUrl !== "") {
editor.update(() => {
const selection:BaseSelection | null = $getSelection();

if ($isRangeSelection(selection)) {
const node:TextNode | ElementNode =
getSelectedNode(selection);
const linkNode:any = $isLinkNode(node)
? node
Comment thread
Mahima-Sanketh-Git marked this conversation as resolved.
Outdated
: node.getParent();

if ($isLinkNode(linkNode)) {
linkNode.setTarget(newTarget);
linkNode.setRel(newTarget === "_blank"
? "noopener noreferrer" : "");
}
}
});
}
}
} }
data-componentid="link-target-checkbox"
/>
}
label={t("flows:core.elements.richText.linkEditor.linkTargetLabel")}
/>
<Tooltip title={linkTarget === "_blank"

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.

Suggested change
<Tooltip title={linkTarget === "_blank"
<Tooltip title={
linkTarget === "_blank"

? t("flows:core.elements.richText.linkEditor.newTabHint")
: t("flows:core.elements.richText.linkEditor.sameTabHint")
} >

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.

Suggested change
} >
}>

<span><Hint></Hint></span>
</Tooltip>

</Box>
<Button
size="small"
variant="outlined"
className="link-input-save-button"
onClick={ (event: ReactMouseEvent<HTMLButtonElement>) => {
event.preventDefault();
if (lastSelection !== null) {
editor.update(()=>{
lastSelection?.clone?.();
});
const currentUrl: string = getCurrentUrl();

if (currentUrl !== "") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@
{
"category": "DISPLAY",
"config": {
"text": "<p class=\"rich-text-paragraph\"><br></p><p class=\"rich-text-paragraph\"><span class=\"rich-text-pre-wrap\">Already have an account? </span><a href=\"{{application.callbackOrAccessUrl}}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"rich-text-link\"><span class=\"rich-text-pre-wrap\">Sign in</span></a></p>"
"text": "<p class=\"rich-text-paragraph\"><br></p><p class=\"rich-text-paragraph\"><span class=\"rich-text-pre-wrap\">Already have an account? </span><a href=\"{{application.callbackOrAccessUrl}}\" class=\"rich-text-link\"><span class=\"rich-text-pre-wrap\">Sign in</span></a></p>",
"linkTarget": "_self"
},
"id": "{{ID}}",
"type": "RICH_TEXT"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,31 @@ const RichTextAdapter = ({ component }) => {

// Resolve placeholders in the HTML content before sanitizing.
const sanitizedHtml = useMemo(() => {
const i18nText = resolveElementText(translations, config.text);
const resolvedHtml = resolvePlaceholders(i18nText || "");

return DOMPurify.sanitize(resolvedHtml, {
ADD_ATTR: [ "target" ]
let html = resolveElementText(translations, config.text);
html = resolvePlaceholders(html || "");

// If linkTarget is configured, update links to use that target
if (config.linkTarget) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const links = doc.querySelectorAll('a');

links.forEach(link => {
link.setAttribute('target', config.linkTarget);
if (config.linkTarget === '_blank') {
link.setAttribute('rel', 'noopener noreferrer');
} else {
link.removeAttribute('rel');
}
});
}, [ config.text, contextData ]);

html = doc.body.innerHTML;
}

return DOMPurify.sanitize(html, {
ADD_ATTR: [ "target", "rel" ]
});
}, [ config.text, config.linkTarget, contextData ]);

return (
<div className="rich-text-content">
Expand Down
4 changes: 4 additions & 0 deletions modules/i18n/src/models/namespaces/flows-ns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ export interface flowsNS {
termsOfUseUrl: string;
};
urlTypeLabel: string;
linkTargetLabel: string;
linkTargetHint: string;
newTabHint: string;
sameTabHint: string;
};
placeholder: string;
};
Expand Down
4 changes: 4 additions & 0 deletions modules/i18n/src/translations/en-US/portals/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export const flows: flowsNS = {
elements: {
richText: {
linkEditor: {
linkTargetHint: "Allow admin to control whether links open in the same tab or new tab",
linkTargetLabel: "Open link in new tab",
newTabHint: "Link will open in a new browser tab",
placeholder: "Enter link URL here...",
predefinedUrls: {
applicationAccessUrl: "Application Access URL",
Expand All @@ -55,6 +58,7 @@ export const flows: flowsNS = {
supportEmail: "Contact Support Email",
termsOfUseUrl: "Terms of Use URL"
},
sameTabHint: "Link will open in the same browser tab",
urlTypeLabel: "URL Type"
},
placeholder: "Enter rich text content here..."
Expand Down
Loading