@@ -49,6 +49,7 @@ interface Schema extends ExtendedFlatSchema {
4949 multiValued ?: boolean ;
5050 mutability ?: string ;
5151 name ?: string ;
52+ regex ?: string ;
5253 required ?: boolean ;
5354 returned ?: string ;
5455 subAttributes ?: Schema [ ] ;
@@ -88,6 +89,7 @@ export interface BaseUserProfileProps {
8889 showFields ?: string [ ] ;
8990
9091 title ?: string ;
92+ userSchema ?: Record < string , any > | null ;
9193}
9294
9395// Fields to skip based on schema.name
@@ -117,14 +119,15 @@ const fieldsToSkip: string[] = [
117119] ;
118120
119121// Fields that should be readonly
120- const readonlyFields : string [ ] = [ 'attributes' , 'id' , 'isReadOnly' , 'ouId' , 'username' ] ;
122+ const readonlyFields : string [ ] = [ 'attributes' , 'id' , 'isReadOnly' , 'ouId' , 'username' , 'sub' ] ;
121123
122124const BaseUserProfile : FC < BaseUserProfileProps > = ( {
123125 fallback = null ,
124126 className = '' ,
125127 cardLayout = true ,
126128 profile,
127129 flattenedProfile,
130+ userSchema,
128131 mode = 'inline' ,
129132 title,
130133 attributeMapping = { } ,
@@ -142,6 +145,7 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
142145 const { theme, colorScheme} = useTheme ( ) ;
143146 const [ editedUser , setEditedUser ] = useState ( flattenedProfile || profile ) ;
144147 const [ editingFields , setEditingFields ] = useState < Record < string , boolean > > ( { } ) ;
148+ const [ fieldErrors , setFieldErrors ] = useState < Record < string , string > > ( { } ) ;
145149 const { t} = useTranslation ( preferences ?. i18n ) ;
146150
147151 useEffect ( ( ) => {
@@ -162,17 +166,13 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
162166 } , [ flattenedProfile , profile , editingFields ] ) ;
163167
164168 /**
165- * Determines if a field should be visible based on showFields, hideFields, and fieldsToSkip arrays.
166- * Priority order:
167- * 1. fieldsToSkip (always hidden) - highest priority
168- * 2. hideFields (explicitly hidden)
169- * 3. showFields (explicitly shown, if array is not empty)
170- * 4. Default behavior (show all fields not in fieldsToSkip)
169+ * Determines if a field should be visible based on showFields, hideFields, and fallback fieldsToSkip arrays.
170+ * When isSchemaBased is true, fieldsToSkip is bypassed so schema-defined fields render dynamically.
171171 */
172- const shouldShowField : any = useCallback (
173- ( fieldName : string ) : boolean => {
174- // Always skip fields in the hardcoded fieldsToSkip array
175- if ( fieldsToSkip . includes ( fieldName ) ) {
172+ const shouldShowField = useCallback (
173+ ( fieldName : string , isSchemaBased : boolean = false ) : boolean => {
174+ // For fallback without schema metadata, skip internal system fields
175+ if ( ! isSchemaBased && fieldsToSkip . includes ( fieldName ) ) {
176176 return false ;
177177 }
178178
@@ -213,22 +213,21 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
213213 } ) ) ;
214214 } , [ ] ) ;
215215
216- const getFieldPlaceholder : any = useCallback ( ( schema : Schema ) : string => {
217- const { type, displayName, description, name} = schema ;
216+ const getFieldPlaceholder : any = useCallback (
217+ ( schema : Schema ) : string => {
218+ const { type, displayName, description, name} = schema ;
218219
219- const fieldLabel : any = displayName || description || name || 'value' ;
220+ const fieldLabel : any = displayName || description || name || 'value' ;
220221
221- switch ( type ) {
222- case 'DATE_TIME' :
223- return `Enter your ${ fieldLabel . toLowerCase ( ) } ` ;
224- case 'BOOLEAN' :
225- return `Select ${ fieldLabel . toLowerCase ( ) } ` ;
226- case 'COMPLEX' :
227- return `Enter ${ fieldLabel . toLowerCase ( ) } details` ;
228- default :
229- return `Enter your ${ fieldLabel . toLowerCase ( ) } ` ;
230- }
231- } , [ ] ) ;
222+ switch ( type ) {
223+ case 'DATE_TIME' :
224+ case 'STRING' :
225+ default :
226+ return t ( 'elements.fields.generic.placeholder' , { field : fieldLabel . toLowerCase ( ) } ) ;
227+ }
228+ } ,
229+ [ t ] ,
230+ ) ;
232231
233232 const formatLabel : any = useCallback (
234233 ( key : string ) : string =>
@@ -285,9 +284,12 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
285284 if ( ! onUpdate || ! schema . name ) return ;
286285
287286 const fieldName : string = schema . name ;
287+ const currentUser : any = flattenedProfile || profile ;
288288 let fieldValue : any ;
289289 if ( editedUser && fieldName && editedUser [ fieldName ] !== undefined ) {
290290 fieldValue = editedUser [ fieldName ] ;
291+ } else if ( currentUser ?. attributes ?. [ fieldName ] !== undefined ) {
292+ fieldValue = currentUser . attributes [ fieldName ] ;
291293 } else if ( flattenedProfile ?. [ fieldName ] !== undefined ) {
292294 fieldValue = flattenedProfile [ fieldName ] ;
293295 } else {
@@ -298,23 +300,64 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
298300 fieldValue = fieldValue . filter ( ( v : any ) => v !== undefined && v !== null && v !== '' ) ;
299301 }
300302
303+ const strVal = String ( fieldValue ?? '' ) . trim ( ) ;
304+ const fieldLabel = schema . displayName || ( schema . name ? startCase ( schema . name ) : 'Field' ) ;
305+
306+ // 1. Required validation
307+ if ( schema . required && ! strVal ) {
308+ setFieldErrors ( ( prev : Record < string , string > ) => ( {
309+ ...prev ,
310+ [ fieldName ] : t ( 'validations.required.field.error' ) ,
311+ } ) ) ;
312+ return ;
313+ }
314+
315+ // 2. Regex validation
316+ if ( schema . regex && strVal ) {
317+ try {
318+ const reg = new RegExp ( schema . regex ) ;
319+ if ( ! reg . test ( strVal ) ) {
320+ setFieldErrors ( ( prev : Record < string , string > ) => ( {
321+ ...prev ,
322+ [ fieldName ] : t ( 'validation.pattern.invalid' ) ,
323+ } ) ) ;
324+ return ;
325+ }
326+ } catch ( e ) {
327+ // ignore invalid regex syntax safely
328+ }
329+ }
330+
331+ // Clear field error if valid
332+ setFieldErrors ( ( prev : Record < string , string > ) => {
333+ const next = { ...prev } ;
334+ delete next [ fieldName ] ;
335+ return next ;
336+ } ) ;
337+
301338 let payload : Record < string , any > = { } ;
302339 set ( payload , fieldName , fieldValue ) ;
303340
304341 onUpdate ( payload ) ;
305342
306343 toggleFieldEdit ( fieldName ) ;
307344 } ,
308- [ editedUser , flattenedProfile , onUpdate , toggleFieldEdit ] ,
345+ [ editedUser , flattenedProfile , profile , onUpdate , toggleFieldEdit , t ] ,
309346 ) ;
310347
311348 const handleFieldCancel : any = useCallback (
312349 ( fieldName : string ) => {
313350 const currentUser : any = flattenedProfile || profile ;
351+ const initialVal = currentUser ?. attributes ?. [ fieldName ] ?? currentUser ?. [ fieldName ] ;
314352 setEditedUser ( ( prev : any ) => ( {
315353 ...prev ,
316- [ fieldName ] : currentUser [ fieldName ] ,
354+ [ fieldName ] : initialVal ,
317355 } ) ) ;
356+ setFieldErrors ( ( prev : Record < string , string > ) => {
357+ const next = { ...prev } ;
358+ delete next [ fieldName ] ;
359+ return next ;
360+ } ) ;
318361 toggleFieldEdit ( fieldName ) ;
319362 } ,
320363 [ flattenedProfile , profile , toggleFieldEdit ] ,
@@ -579,6 +622,11 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
579622 } ,
580623 ( ) => toggleFieldEdit ( schema . name ) ,
581624 ) }
625+ { fieldErrors [ schema . name ] && (
626+ < div style = { { color : '#d32f2f' , fontSize : '0.8rem' , marginTop : '4px' , fontWeight : 500 } } >
627+ { fieldErrors [ schema . name ] }
628+ </ div >
629+ ) }
582630 </ div >
583631 { editable && schema . mutability !== 'READ_ONLY' && ! isReadonlyField && (
584632 < div className = { styles . fieldActions } >
@@ -628,18 +676,54 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
628676
629677 const currentUser : any = flattenedProfile || profile ;
630678
631- const renderProfileWithoutSchemas = ( ) : any => {
679+ const renderProfileContent = ( ) : any => {
632680 if ( ! currentUser ) return null ;
633681
634682 const displayName : any = getDisplayName ( mergedMappings , profile ! , displayNameAttributes ) ;
635683
636- const profileEntries : any = Object . entries ( currentUser )
637- . filter ( ( [ key , value ] : [ string , any ] ) => {
638- if ( ! shouldShowField ( key ) ) return false ;
639-
640- return value !== undefined && value !== '' && value !== null ;
641- } )
642- . sort ( ( [ a ] : [ string , ...any [ ] ] , [ b ] : [ string , ...any [ ] ] ) => a . localeCompare ( b ) ) ;
684+ let schemaItems : Schema [ ] = [ ] ;
685+
686+ if ( userSchema && typeof userSchema === 'object' && Object . keys ( userSchema ) . length > 0 ) {
687+ schemaItems = Object . entries ( userSchema )
688+ . filter ( ( [ key , metaAttr ] : [ string , any ] ) => {
689+ if ( metaAttr ?. credential ) return false ;
690+ return shouldShowField ( key , true ) ;
691+ } )
692+ . map ( ( [ key , metaAttr ] : [ string , any ] ) => {
693+ const val = editedUser ?. [ key ] ?? currentUser ?. attributes ?. [ key ] ?? currentUser ?. [ key ] ?? '' ;
694+
695+ const isReadonly =
696+ metaAttr . readOnly === true || metaAttr . mutability === 'READ_ONLY' || readonlyFields . includes ( key ) ;
697+
698+ return {
699+ name : key ,
700+ displayName : metaAttr . displayName || ( key ? startCase ( key ) : '' ) ,
701+ type : ( metaAttr . type || 'STRING' ) . toUpperCase ( ) ,
702+ regex : metaAttr . regex ,
703+ required : ! ! metaAttr . required ,
704+ mutability : isReadonly ? 'READ_ONLY' : 'READ_WRITE' ,
705+ value : val ,
706+ } ;
707+ } ) ;
708+ } else {
709+ const profileEntries : any = Object . entries ( currentUser )
710+ . filter ( ( [ key , value ] : [ string , any ] ) => {
711+ if ( ! shouldShowField ( key ) ) return false ;
712+
713+ return value !== undefined && value !== '' && value !== null ;
714+ } )
715+ . sort ( ( [ a ] : [ string , ...any [ ] ] , [ b ] : [ string , ...any [ ] ] ) => a . localeCompare ( b ) ) ;
716+
717+ schemaItems = profileEntries . map ( ( [ key , value ] : any ) => {
718+ const isReadonly = readonlyFields . includes ( key ) ;
719+ return {
720+ name : key ,
721+ displayName : startCase ( key ) ,
722+ mutability : isReadonly ? 'READ_ONLY' : 'READ_WRITE' ,
723+ value,
724+ } ;
725+ } ) ;
726+ }
643727
644728 return (
645729 < >
@@ -661,17 +745,11 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
661745 ) }
662746 </ div >
663747 < Divider />
664- { profileEntries . map ( ( [ key , value ] : any ) => {
665- const isReadonly = readonlyFields . includes ( key ) ;
666- const schema : Schema = { name : key , mutability : isReadonly ? 'READ_ONLY' : 'READ_WRITE' } ;
667- const schemaWithValue : any = { ...schema , value} ;
668-
669- return (
670- < div key = { key } className = { styles . info } >
671- { renderUserInfo ( schemaWithValue ) }
672- </ div >
673- ) ;
674- } ) }
748+ { schemaItems . map ( ( schemaWithValue : Schema ) => (
749+ < div key = { schemaWithValue . name } className = { styles . info } >
750+ { renderUserInfo ( schemaWithValue ) }
751+ </ div >
752+ ) ) }
675753 </ >
676754 ) ;
677755 } ;
@@ -687,7 +765,7 @@ const BaseUserProfile: FC<BaseUserProfileProps> = ({
687765 < AlertPrimitive . Description > { error } </ AlertPrimitive . Description >
688766 </ AlertPrimitive >
689767 ) }
690- < div className = { styles . infoContainer } > { renderProfileWithoutSchemas ( ) } </ div >
768+ < div className = { styles . infoContainer } > { renderProfileContent ( ) } </ div >
691769 </ CardPrimitive >
692770 ) ;
693771
0 commit comments