11import type { Credential , AttributeRequest , Schema } from '../../../../types'
2+ import type { AuthContextProps } from 'react-oidc-context'
23
3- import { useEffect , useState } from 'react'
4+ import { useEffect , useRef , useState } from 'react'
45import { useAuth } from 'react-oidc-context'
56
67import { getSchemaById , publicBaseUrl } from '../../../../api/adminApi'
78import { truncateLongString } from '../../../../utils/formatters'
89import logger from '../../../../utils/logger'
910
11+ /* -------------------------------------------------------
12+ Schema hook (NO caching, race-safe only)
13+ ------------------------------------------------------- */
14+ function useSchema ( auth : AuthContextProps , schemaId ?: string ) {
15+ const [ schema , setSchema ] = useState < Schema | null > ( null )
16+
17+ const requestIdRef = useRef ( 0 )
18+
19+ useEffect ( ( ) => {
20+ if ( ! schemaId || ! auth ?. isAuthenticated ) {
21+ setSchema ( null )
22+ return
23+ }
24+
25+ let cancelled = false
26+ const requestId = ++ requestIdRef . current
27+
28+ const run = async ( ) => {
29+ try {
30+ const result = await getSchemaById ( auth , schemaId )
31+
32+ if ( cancelled || requestId !== requestIdRef . current ) return
33+
34+ setSchema ( result )
35+ } catch ( err ) {
36+ logger . error ( 'Error fetching schema:' , err )
37+ if ( cancelled || requestId !== requestIdRef . current ) return
38+ setSchema ( null )
39+ }
40+ }
41+
42+ run ( )
43+
44+ return ( ) => {
45+ cancelled = true
46+ }
47+ } , [ schemaId , auth . isAuthenticated ] )
48+
49+ return schema
50+ }
51+
52+ /* -------------------------------------------------------
53+ Pure derivation (NO state)
54+ ------------------------------------------------------- */
55+ function getPredicateUIState ( schema : Schema | null , selectedAttributes : Map < string , AttributeRequest > ) {
56+ const dateOptions : Record < string , 'custom' | 'relative' > = { }
57+ const yearOffsets : Record < string , number > = { }
58+
59+ if ( ! schema ) return { dateOptions, yearOffsets }
60+
61+ for ( const attr of schema . attributes ) {
62+ const request = selectedAttributes . get ( attr . name )
63+ if ( ! request ?. predicate ) continue
64+
65+ const value = request . predicateValue
66+
67+ if ( attr . type === 'date' ) {
68+ if ( value ?. startsWith ( '$dateint:' ) ) {
69+ dateOptions [ attr . name ] = 'relative'
70+ const parsed = parseInt ( value . replace ( '$dateint:' , '' ) , 10 )
71+ yearOffsets [ attr . name ] = Number . isNaN ( parsed ) ? 0 : parsed
72+ } else if ( value ) {
73+ dateOptions [ attr . name ] = 'custom'
74+ }
75+ }
76+ }
77+
78+ return { dateOptions, yearOffsets }
79+ }
80+
81+ /* -------------------------------------------------------
82+ Component
83+ ------------------------------------------------------- */
1084interface SelectingAttributesStepProps {
1185 currentCredential : Credential | null
1286 selectedAttributes : Map < string , AttributeRequest >
@@ -35,51 +109,16 @@ export function SelectingAttributesStep({
35109 onClose,
36110} : SelectingAttributesStepProps ) {
37111 const auth = useAuth ( )
38- const [ schema , setSchema ] = useState < Schema | null > ( null )
39- const [ predicateDateOptions , setPredicateDateOptions ] = useState < Record < string , 'custom' | 'relative' > > ( { } )
40- const [ predicateYearOffsets , setPredicateYearOffsets ] = useState < Record < string , number > > ( { } )
41112
42- useEffect ( ( ) => {
43- const fetchSchema = async ( ) => {
44- if ( currentCredential ?. schema_id && auth . isAuthenticated ) {
45- try {
46- const schemaData = await getSchemaById ( auth , currentCredential . schema_id )
47- setSchema ( schemaData )
48-
49- // Initialize predicate date options from selectedAttributes
50- const newDateOptions : Record < string , 'custom' | 'relative' > = { }
51- const newYearOffsets : Record < string , number > = { }
52-
53- schemaData . attributes . forEach ( ( attr : { name : string ; type : string } ) => {
54- if ( attr . type === 'date' ) {
55- // Check all selected attributes for this credential
56- selectedAttributes . forEach ( ( request , attrName ) => {
57- if ( attrName === attr . name && request . predicate ) {
58- const value = request . predicateValue
59- if ( value && value . startsWith ( '$dateint:' ) ) {
60- const yearOffset = parseInt ( value . replace ( '$dateint:' , '' ) , 10 )
61- newDateOptions [ attrName ] = 'relative'
62- newYearOffsets [ attrName ] = yearOffset
63- } else if ( value ) {
64- newDateOptions [ attrName ] = 'custom'
65- }
66- }
67- } )
68- }
69- } )
70-
71- setPredicateDateOptions ( newDateOptions )
72- setPredicateYearOffsets ( newYearOffsets )
73- } catch ( error ) {
74- logger . error ( 'Error fetching schema:' , error )
75- setSchema ( null )
76- }
77- }
78- }
79- fetchSchema ( )
80- } , [ currentCredential , selectedAttributes , auth . isAuthenticated ] )
113+ const schema = useSchema ( auth , currentCredential ?. schema_id )
114+
115+ const { dateOptions : predicateDateOptions , yearOffsets : predicateYearOffsets } = getPredicateUIState (
116+ schema ,
117+ selectedAttributes ,
118+ )
81119
82120 if ( ! currentCredential ) return null
121+
83122 const isAttributeSelected = ( attrName : string ) => {
84123 const request = selectedAttributes . get ( attrName )
85124 return request && ( request . property || request . predicate || request . nonRevoked )
@@ -101,6 +140,7 @@ export function SelectingAttributesStep({
101140 < p className = "text-xs text-gray-500" > v{ currentCredential . version } </ p >
102141 </ div >
103142 </ div >
143+
104144 < p className = "text-sm text-gray-600" > Configure how each attribute should be requested</ p >
105145 </ div >
106146
@@ -113,10 +153,19 @@ export function SelectingAttributesStep({
113153 return (
114154 < div key = { attr . name } className = "border border-gray-200 rounded-lg p-4" >
115155 < div className = "flex items-start justify-between mb-3" >
116- < div >
156+ < div className = "flex-1" >
117157 < p className = "text-sm font-medium text-bcgov-black" > { attr . name } </ p >
118- < p className = "text-xs text-gray-500" > { truncateLongString ( attr . value ) } </ p >
158+ { typeof attr . value === 'string' && attr . value . startsWith ( 'data:image' ) ? (
159+ < img
160+ src = { attr . value }
161+ alt = { attr . name }
162+ className = "max-w-xs max-h-48 mt-2 rounded-lg border border-gray-200"
163+ />
164+ ) : (
165+ < p className = "text-xs text-gray-500" > { truncateLongString ( attr . value ) } </ p >
166+ ) }
119167 </ div >
168+
120169 { selected && (
121170 < button
122171 onClick = { ( ) => onRemoveAttribute ( attr . name ) }
@@ -134,6 +183,7 @@ export function SelectingAttributesStep({
134183 checked = { request . property || false }
135184 onChange = { ( e ) => {
136185 const updated = { ...request , property : e . target . checked }
186+
137187 if ( e . target . checked || request . predicate || request . nonRevoked ) {
138188 onUpdateAttribute ( attr . name , updated )
139189 } else {
@@ -154,15 +204,16 @@ export function SelectingAttributesStep({
154204 checked = { request . predicate || false }
155205 onChange = { ( e ) => {
156206 const updated = { ...request , predicate : e . target . checked }
207+
157208 if ( ! e . target . checked ) {
158209 delete updated . predicateType
159210 delete updated . predicateValue
160211 } else {
161- // When checking the predicate box, set defaults
162212 if ( ! updated . predicateType ) {
163213 updated . predicateType = '>='
164214 }
165215 }
216+
166217 if ( e . target . checked || request . property || request . nonRevoked ) {
167218 onUpdateAttribute ( attr . name , updated )
168219 } else {
@@ -181,10 +232,14 @@ export function SelectingAttributesStep({
181232 < div className = "ml-7 space-y-3 p-3 bg-blue-50 rounded-lg border border-blue-100" >
182233 < div >
183234 < label className = "block text-xs font-medium text-gray-700 mb-1" > Operator</ label >
235+
184236 < select
185237 value = { request . predicateType || '' }
186238 onChange = { ( e ) => {
187- const updated = { ...request , predicateType : e . target . value as any }
239+ const updated = {
240+ ...request ,
241+ predicateType : e . target . value as any ,
242+ }
188243 onUpdateAttribute ( attr . name , updated )
189244 } }
190245 className = "w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-bcgov-blue focus:border-transparent"
@@ -195,16 +250,19 @@ export function SelectingAttributesStep({
195250 < option value = "<" > Less Than (<)</ option >
196251 </ select >
197252 </ div >
253+
198254 < div >
199255 < label className = "block text-xs font-medium text-gray-700 mb-1" > Value</ label >
200- { schema && schema . attributes . find ( ( a : any ) => a . name === attr . name ) ?. type === 'date' ? (
256+
257+ { schema ?. attributes . find ( ( a ) => a . name === attr . name ) ?. type === 'date' ? (
201258 < div className = "space-y-3" >
202259 < p
203260 className = "text-xs font-semibold text-gray-700 uppercase tracking-wide"
204261 hidden = { predicateDateOptions [ attr . name ] === 'relative' }
205262 >
206263 Select an explicit date
207264 </ p >
265+
208266 < p
209267 className = "text-xs font-semibold text-gray-700 uppercase tracking-wide"
210268 hidden = {
@@ -214,41 +272,51 @@ export function SelectingAttributesStep({
214272 >
215273 Years relative to presentation
216274 </ p >
275+
217276 < input
218277 type = "date"
219278 value = { request . predicateValue || '' }
220279 onChange = { ( e ) => {
221- const updated = { ...request , predicateValue : e . target . value }
280+ const updated = {
281+ ...request ,
282+ predicateValue : e . target . value ,
283+ }
222284 onUpdateAttribute ( attr . name , updated )
223285 } }
224- placeholder = "Enter the date to compare against"
225286 className = "w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-bcgov-blue focus:border-transparent"
226287 hidden = { predicateDateOptions [ attr . name ] === 'relative' }
227288 />
289+
228290 < input
229291 type = "number"
230292 value = { predicateYearOffsets [ attr . name ] ?? 0 }
231293 onChange = { ( e ) => {
232294 const parsed = e . target . value === '' ? 0 : parseInt ( e . target . value , 10 )
295+
233296 const numVal = Number . isNaN ( parsed ) ? 0 : parsed
234- setPredicateYearOffsets ( ( prev ) => ( { ...prev , [ attr . name ] : numVal } ) )
235- const updated = { ...request , predicateValue : `$dateint:${ numVal } ` }
297+
298+ const updated = {
299+ ...request ,
300+ predicateValue : `$dateint:${ numVal } ` ,
301+ }
302+
236303 onUpdateAttribute ( attr . name , updated )
237304 } }
238- placeholder = "Years offset (positive or negative)"
239305 className = "w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-bcgov-blue focus:border-transparent"
240306 hidden = {
241307 predicateDateOptions [ attr . name ] === 'custom' ||
242308 predicateDateOptions [ attr . name ] === undefined
243309 }
244310 />
311+
245312 < div className = "flex gap-2" >
246313 < button
247314 type = "button"
248315 onClick = { ( ) => {
249- setPredicateDateOptions ( ( prev ) => ( { ...prev , [ attr . name ] : 'custom' } ) )
250- setPredicateYearOffsets ( ( prev ) => ( { ...prev , [ attr . name ] : 0 } ) )
251- const updated = { ...request , predicateValue : '' }
316+ const updated = {
317+ ...request ,
318+ predicateValue : '' ,
319+ }
252320 onUpdateAttribute ( attr . name , updated )
253321 } }
254322 className = { `flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-colors ${
@@ -259,13 +327,17 @@ export function SelectingAttributesStep({
259327 >
260328 Custom Date
261329 </ button >
330+
262331 < button
263332 type = "button"
264333 onClick = { ( ) => {
265334 const currentOffset = predicateYearOffsets [ attr . name ] ?? 0
266- setPredicateDateOptions ( ( prev ) => ( { ...prev , [ attr . name ] : 'relative' } ) )
267- setPredicateYearOffsets ( ( prev ) => ( { ...prev , [ attr . name ] : currentOffset } ) )
268- const updated = { ...request , predicateValue : `$dateint:${ currentOffset } ` }
335+
336+ const updated = {
337+ ...request ,
338+ predicateValue : `$dateint:${ currentOffset } ` ,
339+ }
340+
269341 onUpdateAttribute ( attr . name , updated )
270342 } }
271343 className = { `flex-1 px-3 py-2 text-xs font-medium rounded-lg transition-colors ${
@@ -283,10 +355,12 @@ export function SelectingAttributesStep({
283355 type = "text"
284356 value = { request . predicateValue || '' }
285357 onChange = { ( e ) => {
286- const updated = { ...request , predicateValue : e . target . value }
358+ const updated = {
359+ ...request ,
360+ predicateValue : e . target . value ,
361+ }
287362 onUpdateAttribute ( attr . name , updated )
288363 } }
289- placeholder = "Enter the value to compare against"
290364 className = "w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-bcgov-blue focus:border-transparent"
291365 />
292366 ) }
@@ -299,7 +373,11 @@ export function SelectingAttributesStep({
299373 type = "checkbox"
300374 checked = { request . nonRevoked || false }
301375 onChange = { ( e ) => {
302- const updated = { ...request , nonRevoked : e . target . checked }
376+ const updated = {
377+ ...request ,
378+ nonRevoked : e . target . checked ,
379+ }
380+
303381 if ( e . target . checked || request . property || request . predicate ) {
304382 onUpdateAttribute ( attr . name , updated )
305383 } else {
@@ -333,6 +411,7 @@ export function SelectingAttributesStep({
333411 </ button >
334412 ) }
335413 </ div >
414+
336415 < div className = "flex gap-3" >
337416 { currentIndex > 0 && (
338417 < button
@@ -342,8 +421,7 @@ export function SelectingAttributesStep({
342421 Previous
343422 </ button >
344423 ) }
345- </ div >
346- < div className = "flex gap-3" >
424+
347425 { currentIndex < totalCredentials - 1 && (
348426 < button
349427 onClick = { onNext }
@@ -352,6 +430,7 @@ export function SelectingAttributesStep({
352430 Next
353431 </ button >
354432 ) }
433+
355434 { currentIndex === totalCredentials - 1 && (
356435 < button
357436 onClick = { onContinue }
0 commit comments