Skip to content
Open
Changes from all 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
210 changes: 161 additions & 49 deletions features/admin.server-configurations.v1/forms/multi-attribute-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@
* specific language governing permissions and limitations
* under the License.
*/

import { IdentifiableComponentInterface } from "@wso2is/core/models";
import Autocomplete from "@oxygen-ui/react/Autocomplete";
import Chip from "@oxygen-ui/react/Chip";
import TextField from "@oxygen-ui/react/TextField";
import Tooltip from "@oxygen-ui/react/Tooltip";
import { Claim, ClaimsGetParams, IdentifiableComponentInterface } from "@wso2is/core/models";
import { Field, Form } from "@wso2is/form";
import isEmpty from "lodash-es/isEmpty";
import React, { FunctionComponent, ReactElement, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ServerConfigurationsConstants } from "../constants/server-configurations-constants";
import useGetAllLocalClaims from "../../admin.claims.v1/api/use-get-all-local-claims";
import {
ConnectorPropertyInterface,
GovernanceConnectorInterface } from "../models/governance-connectors";
GovernanceConnectorInterface
} from "../models/governance-connectors";
import { GovernanceConnectorUtils } from "../utils/governance-connector-utils";

/**
Expand Down Expand Up @@ -76,15 +80,83 @@ export const MultiAttributeLoginForm: FunctionComponent<MultiAttributeLoginFormP
} = props;

const { t } = useTranslation();
const containerStyle: React.CSSProperties = { marginBottom: "1rem" };
const labelStyle: React.CSSProperties = {
display: "block",
fontSize: "0.875rem",
fontWeight: 500,
marginBottom: "0.5rem"
};
const spanStyle: React.CSSProperties = {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
};
const helperTextStyle: React.CSSProperties = {
color: "rgba(0, 0, 0, 0.6)",
fontSize: "0.75rem",
marginTop: "0.25rem"
};

const [ initialConnectorValues, setInitialConnectorValues ]
= useState<Map<string, ConnectorPropertyInterface>>(undefined);
const [ initialFormValues, setInitialFormValues ]
= useState<any>(undefined);
const [ selectedClaims, setSelectedClaims ] = useState<string[]>([]);
const [ claimURIs, setClaimURIs ] = useState<string[]>([]);
const [ claimMap, setClaimMap ] = useState<Map<string, string>>(new Map());

const claimsParams: ClaimsGetParams = {
filter: "",
limit: 1000,
offset: 0,
sort: ""
};
const {
data: claimsData,
isLoading: isClaimsLoading
} = useGetAllLocalClaims<Claim[]>(claimsParams, isConnectorEnabled);

useEffect(() => {
if (claimsData && !isClaimsLoading) {
const uris: string[] = getClaimURIs(claimsData);
const map: Map<string, string> = createClaimMap(claimsData);

setClaimURIs(uris);
setClaimMap(map);
}
}, [ claimsData, isClaimsLoading ]);

const getClaimURIs = (claims: Claim[]): string[] => {
if (!claims || claims.length === 0) {
return [];
}

return claims
.map((claim: Claim) => claim?.claimURI)
.filter((uri: string) => typeof uri === "string" && uri.length > 0);
};
const createClaimMap = (claims: Claim[]): Map<string, string> => {
const map: Map<string, string> = new Map<string, string>();

if (!claims || claims.length === 0) {
return map;
}
claims.forEach((claim: Claim) => {
if (claim?.claimURI && claim?.displayName) {
map.set(claim.claimURI, claim.displayName);
}
});

return map;
};

const getDisplayName = (uri:string): string => {
return claimMap.get(uri) ||uri;
};

/**
* Flattens and resolved form initial values and field metadata.
*/

Copy link
Member

Choose a reason for hiding this comment

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

Suggested change

useEffect(() => {

if (isEmpty(initialValues?.properties)) {
Expand All @@ -101,12 +173,20 @@ export const MultiAttributeLoginForm: FunctionComponent<MultiAttributeLoginFormP
resolvedInitialValues.set(property.name, property);
resolvedInitialFormValues = {
...resolvedInitialFormValues,
[ property.name ]: property.value
[property.name]: property.value
Comment on lines -104 to +176
Copy link
Member

Choose a reason for hiding this comment

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

Is this intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no , i accidentally added space

};
});

setInitialConnectorValues(resolvedInitialValues);
setInitialFormValues(resolvedInitialFormValues);

const allowedAttrs: string =
resolvedInitialFormValues?.["account.multiattributelogin.handler.allowedattributes"];

if (allowedAttrs) {
const attrsArray: string[] = allowedAttrs.split(",").map((attr: string) => attr.trim());

setSelectedClaims(attrsArray);
}
}, [ initialValues ]);

/**
Expand All @@ -115,21 +195,25 @@ export const MultiAttributeLoginForm: FunctionComponent<MultiAttributeLoginFormP
* @param values - Form values.
* @returns Sanitized form values.
*/
const getUpdatedConfigurations = (values: Record<string, unknown>) => {
const getUpdatedConfigurations = (_values: Record<string, unknown>) => {
let data: { [key: string]: unknown } = {};
const claimsValue: string = selectedClaims.join(",");

for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)
&& key !== ServerConfigurationsConstants.MULTI_ATTRIBUTE_LOGIN_ENABLE) {
data = {
...data,
[ GovernanceConnectorUtils.decodeConnectorPropertyName(key) ]: values[ key ]
};
}
}
data = {
"account.multiattributelogin.handler.allowedattributes": claimsValue
};

return data;
};
/**
* Handle autocomplete change.
*
* @param event - Change event.
* @param newValue - New selected values.
*/
const handleClaimChange = (event: React.SyntheticEvent, newValue: string[]): void => {
setSelectedClaims(newValue);
};

if (!initialConnectorValues) {
return null;
Expand All @@ -138,47 +222,75 @@ export const MultiAttributeLoginForm: FunctionComponent<MultiAttributeLoginFormP
return (
<Form
id={ FORM_ID }
uncontrolledForm={ true }
initialValues={ initialFormValues }
uncontrolledForm={ false }
onSubmit={ (values: Record<string, unknown>) =>
onSubmit(getUpdatedConfigurations(values))
}
>
<Field.Input
ariaLabel="account.multiattributelogin.handler.allowedattributes"
inputType="text"
name={ GovernanceConnectorUtils.encodeConnectorPropertyName(
"account.multiattributelogin.handler.allowedattributes"
) }
type="text"
width={ 16 }
required={ true }
placeholder={ "Enter allowed attribute list" }
labelPosition="top"
minLength={ 3 }
maxLength={ 1000 }
readOnly={ readOnly }
initialValue={ initialFormValues?.[ "account.multiattributelogin.handler.allowedattributes" ] }
data-componentid={ `${ componentId }-allowed-attribute-list` }
label={ GovernanceConnectorUtils.resolveFieldLabel(
"Account Management",
"account.multiattributelogin.handler.allowedattributes",
"Allowed Attribute List")
}
disabled={ !isConnectorEnabled }
hint={ GovernanceConnectorUtils.resolveFieldHint(
"Account Management",
"account.multiattributelogin.handler.allowedattributes",
"Allowed claim list separated by commas.")
}
/>
<div style={ containerStyle }>
<label style={ labelStyle }>
{ GovernanceConnectorUtils.resolveFieldLabel(
"Account Management",
"account.multiattributelogin.handler.allowedattributes",
"Allowed Attribute List"
) }
</label>
<Autocomplete
multiple
options={ claimURIs }
value={ selectedClaims }
onChange={ handleClaimChange }
disabled={ !isConnectorEnabled || readOnly || isClaimsLoading }
loading={ isClaimsLoading }
renderInput={ (params: any) => (
<TextField
{ ...params }
placeholder={ isClaimsLoading ? "Loading claims..." : "Select claim URIs" }
size="small"
data-componentid={ `${componentId}-allowed-attribute-list` }
/>
) }
renderOption={ (props: React.HTMLAttributes<HTMLLIElement>, option: string) => (
<li { ...props } key={ option }>
<Tooltip title={ option } placement="right">
<span style={ spanStyle }>
{ getDisplayName(option) }
</span>
</Tooltip>
</li>
) }
renderTags={ (value: string[], getTagProps: (args: { index: number }) => any) =>
value.map((option: string, index: number) => (
<Tooltip title={ option } key={ option } placement="top">
<Chip
{ ...getTagProps({ index }) }
label={ getDisplayName(option) }
size="small"
key={ option }
/>
</Tooltip>
))
}
Comment on lines +262 to +273
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate key prop on Chip component.

getTagProps({ index }) already provides a key prop. Adding an explicit key={ option } on the Chip causes duplicate key assignment. Remove the explicit key from Chip to avoid potential React warnings.

Proposed fix
     renderTags={ (value: string[], getTagProps: (args: { index: number }) => any) =>
         value.map((option: string, index: number) => (
             <Tooltip title={ option } key={ option } placement="top">
                 <Chip
                     { ...getTagProps({ index }) }
                     label={ getDisplayName(option) }
                     size="small"
-                    key={ option }
                 />
             </Tooltip>
         ))
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
renderTags={ (value: string[], getTagProps: (args: { index: number }) => any) =>
value.map((option: string, index: number) => (
<Tooltip title={ option } key={ option } placement="top">
<Chip
{ ...getTagProps({ index }) }
label={ getDisplayName(option) }
size="small"
key={ option }
/>
</Tooltip>
))
}
renderTags={ (value: string[], getTagProps: (args: { index: number }) => any) =>
value.map((option: string, index: number) => (
<Tooltip title={ option } key={ option } placement="top">
<Chip
{ ...getTagProps({ index }) }
label={ getDisplayName(option) }
size="small"
/>
</Tooltip>
))
}
🤖 Prompt for AI Agents
In @features/admin.server-configurations.v1/forms/multi-attribute-login.tsx
around lines 265-276, The Chip inside renderTags is receiving a duplicate key
because getTagProps({ index }) already injects a key; remove the explicit key={
option } from the Chip component (keep the spread {...getTagProps({ index })}
and the Tooltip's key if needed) so only one key prop is present; locate the
renderTags function and edit the Chip JSX to drop key={ option } while
preserving label={getDisplayName(option)} and size="small".

freeSolo={ false }
disableCloseOnSelect
data-componentid={ `${componentId}-autocomplete` }
/>
<div style={ helperTextStyle }>
{ GovernanceConnectorUtils.resolveFieldHint(
"Account Management",
"account.multiattributelogin.handler.allowedattributes",
"Select one or more claim URIs from the list."
) }
</div>
</div>

<Field.Button
form={ FORM_ID }
size="small"
buttonType="primary_btn"
ariaLabel="Self registration update button"
name="update-button"
data-componentid={ `${ componentId }-submit-button` }
data-componentid={ `${componentId}-submit-button` }
disabled={ !isConnectorEnabled || isSubmitting }
loading={ isSubmitting }
label={ t("common:update") }
Expand Down
Loading