Skip to content

Commit ad18b23

Browse files
authored
refactor: Proof editor modal state and re-rendering (#534)
* refactor: Proof editor modal state and re-rendering Signed-off-by: jamshale <jamiehalebc@gmail.com> * Copilot review findings Signed-off-by: jamshale <jamiehalebc@gmail.com> --------- Signed-off-by: jamshale <jamiehalebc@gmail.com>
1 parent 5251f7c commit ad18b23

1 file changed

Lines changed: 143 additions & 64 deletions

File tree

frontend/src/admin/components/showcase/modals/steps/SelectingAttributesStep.tsx

Lines changed: 143 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,86 @@
11
import 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'
45
import { useAuth } from 'react-oidc-context'
56

67
import { getSchemaById, publicBaseUrl } from '../../../../api/adminApi'
78
import { truncateLongString } from '../../../../utils/formatters'
89
import 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+
------------------------------------------------------- */
1084
interface 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 (&lt;)</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

Comments
 (0)