@ic-reactor/candid 3.0.12-beta.0 → 3.0.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/metadata-display-reactor.d.ts.map +1 -1
- package/dist/metadata-display-reactor.js +2 -2
- package/dist/metadata-display-reactor.js.map +1 -1
- package/dist/visitor/arguments/helpers.d.ts +40 -0
- package/dist/visitor/arguments/helpers.d.ts.map +1 -0
- package/dist/visitor/arguments/helpers.js +81 -0
- package/dist/visitor/arguments/helpers.js.map +1 -0
- package/dist/visitor/arguments/index.d.ts +19 -6
- package/dist/visitor/arguments/index.d.ts.map +1 -1
- package/dist/visitor/arguments/index.js +343 -24
- package/dist/visitor/arguments/index.js.map +1 -1
- package/dist/visitor/arguments/types.d.ts +183 -178
- package/dist/visitor/arguments/types.d.ts.map +1 -1
- package/dist/visitor/arguments/types.js +1 -40
- package/dist/visitor/arguments/types.js.map +1 -1
- package/dist/visitor/returns/types.d.ts +3 -4
- package/dist/visitor/returns/types.d.ts.map +1 -1
- package/dist/visitor/types.d.ts +14 -0
- package/dist/visitor/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/metadata-display-reactor.ts +2 -2
- package/src/visitor/arguments/helpers.ts +104 -0
- package/src/visitor/arguments/index.test.ts +443 -23
- package/src/visitor/arguments/index.ts +410 -41
- package/src/visitor/arguments/schema.test.ts +117 -7
- package/src/visitor/arguments/types.ts +284 -284
- package/src/visitor/returns/types.ts +4 -27
- package/src/visitor/types.ts +45 -0
- package/src/visitor/arguments/README.md +0 -230
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isQuery } from "../helpers"
|
|
2
|
+
import { checkTextFormat, checkNumberFormat } from "../constants"
|
|
2
3
|
import type {
|
|
3
|
-
|
|
4
|
+
FieldNode,
|
|
4
5
|
RecordField,
|
|
5
6
|
VariantField,
|
|
6
7
|
TupleField,
|
|
@@ -16,17 +17,119 @@ import type {
|
|
|
16
17
|
UnknownField,
|
|
17
18
|
ArgumentsMeta,
|
|
18
19
|
ArgumentsServiceMeta,
|
|
20
|
+
RenderHint,
|
|
21
|
+
PrimitiveInputProps,
|
|
22
|
+
BlobLimits,
|
|
23
|
+
BlobValidationResult,
|
|
24
|
+
TextFormat,
|
|
25
|
+
NumberFormat,
|
|
19
26
|
} from "./types"
|
|
20
27
|
|
|
21
28
|
import { IDL } from "@icp-sdk/core/candid"
|
|
22
29
|
import { Principal } from "@icp-sdk/core/principal"
|
|
23
30
|
import { BaseActor, FunctionName } from "@ic-reactor/core"
|
|
24
31
|
import * as z from "zod"
|
|
32
|
+
import { formatLabel } from "./helpers"
|
|
25
33
|
|
|
26
34
|
export * from "./types"
|
|
35
|
+
export * from "./helpers"
|
|
36
|
+
export { checkTextFormat, checkNumberFormat } from "../constants"
|
|
37
|
+
|
|
38
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
// Render Hint Helpers
|
|
40
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
41
|
+
|
|
42
|
+
const COMPOUND_RENDER_HINT: RenderHint = {
|
|
43
|
+
isCompound: true,
|
|
44
|
+
isPrimitive: false,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const TEXT_RENDER_HINT: RenderHint = {
|
|
48
|
+
isCompound: false,
|
|
49
|
+
isPrimitive: true,
|
|
50
|
+
inputType: "text",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const NUMBER_RENDER_HINT: RenderHint = {
|
|
54
|
+
isCompound: false,
|
|
55
|
+
isPrimitive: true,
|
|
56
|
+
inputType: "number",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CHECKBOX_RENDER_HINT: RenderHint = {
|
|
60
|
+
isCompound: false,
|
|
61
|
+
isPrimitive: true,
|
|
62
|
+
inputType: "checkbox",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const FILE_RENDER_HINT: RenderHint = {
|
|
66
|
+
isCompound: false,
|
|
67
|
+
isPrimitive: true,
|
|
68
|
+
inputType: "file",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// Blob Field Helpers
|
|
73
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
const DEFAULT_BLOB_LIMITS: BlobLimits = {
|
|
76
|
+
maxHexBytes: 512,
|
|
77
|
+
maxFileBytes: 2 * 1024 * 1024, // 2MB
|
|
78
|
+
maxHexDisplayLength: 128,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeHex(input: string): string {
|
|
82
|
+
// Remove 0x prefix and convert to lowercase
|
|
83
|
+
let hex = input.toLowerCase()
|
|
84
|
+
if (hex.startsWith("0x")) {
|
|
85
|
+
hex = hex.slice(2)
|
|
86
|
+
}
|
|
87
|
+
// Remove any whitespace
|
|
88
|
+
hex = hex.replace(/\s/g, "")
|
|
89
|
+
return hex
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateBlobInput(
|
|
93
|
+
value: string | Uint8Array,
|
|
94
|
+
limits: BlobLimits
|
|
95
|
+
): BlobValidationResult {
|
|
96
|
+
if (value instanceof Uint8Array) {
|
|
97
|
+
if (value.length > limits.maxFileBytes) {
|
|
98
|
+
return {
|
|
99
|
+
valid: false,
|
|
100
|
+
error: `File size exceeds maximum of ${limits.maxFileBytes} bytes`,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { valid: true }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// String input (hex)
|
|
107
|
+
const normalized = normalizeHex(value)
|
|
108
|
+
if (normalized.length === 0) {
|
|
109
|
+
return { valid: true } // Empty is valid
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!/^[0-9a-f]*$/.test(normalized)) {
|
|
113
|
+
return { valid: false, error: "Invalid hex characters" }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (normalized.length % 2 !== 0) {
|
|
117
|
+
return { valid: false, error: "Hex string must have even length" }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const byteLength = normalized.length / 2
|
|
121
|
+
if (byteLength > limits.maxHexBytes) {
|
|
122
|
+
return {
|
|
123
|
+
valid: false,
|
|
124
|
+
error: `Hex input exceeds maximum of ${limits.maxHexBytes} bytes`,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { valid: true }
|
|
129
|
+
}
|
|
27
130
|
|
|
28
131
|
/**
|
|
29
|
-
*
|
|
132
|
+
* FieldVisitor generates metadata for form input fields from Candid IDL types.
|
|
30
133
|
*
|
|
31
134
|
* ## Design Principles
|
|
32
135
|
*
|
|
@@ -40,8 +143,11 @@ export * from "./types"
|
|
|
40
143
|
*
|
|
41
144
|
* Each field has:
|
|
42
145
|
* - `type`: The field type (record, variant, text, number, etc.)
|
|
43
|
-
* - `label`:
|
|
146
|
+
* - `label`: Raw label from Candid
|
|
147
|
+
* - `displayLabel`: Human-readable formatted label
|
|
44
148
|
* - `name`: TanStack Form compatible path (e.g., "[0]", "[0].owner", "tags[1]")
|
|
149
|
+
* - `component`: Suggested component type for rendering
|
|
150
|
+
* - `renderHint`: Hints for UI rendering strategy
|
|
45
151
|
* - `defaultValue`: Initial value for the form
|
|
46
152
|
* - `schema`: Zod schema for validation
|
|
47
153
|
* - Type-specific properties (options for variant, fields for record, etc.)
|
|
@@ -52,9 +158,9 @@ export * from "./types"
|
|
|
52
158
|
* @example
|
|
53
159
|
* ```typescript
|
|
54
160
|
* import { useForm } from '@tanstack/react-form'
|
|
55
|
-
* import {
|
|
161
|
+
* import { FieldVisitor } from '@ic-reactor/candid'
|
|
56
162
|
*
|
|
57
|
-
* const visitor = new
|
|
163
|
+
* const visitor = new FieldVisitor()
|
|
58
164
|
* const serviceMeta = service.accept(visitor, null)
|
|
59
165
|
* const methodMeta = serviceMeta["icrc1_transfer"]
|
|
60
166
|
*
|
|
@@ -74,9 +180,9 @@ export * from "./types"
|
|
|
74
180
|
* ))
|
|
75
181
|
* ```
|
|
76
182
|
*/
|
|
77
|
-
export class
|
|
183
|
+
export class FieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
78
184
|
string,
|
|
79
|
-
|
|
185
|
+
FieldNode | ArgumentsMeta<A> | ArgumentsServiceMeta<A>
|
|
80
186
|
> {
|
|
81
187
|
public recursiveSchemas: Map<string, z.ZodTypeAny> = new Map()
|
|
82
188
|
|
|
@@ -130,7 +236,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
130
236
|
const fields = t.argTypes.map((arg, index) => {
|
|
131
237
|
return this.withName(`[${index}]`, () =>
|
|
132
238
|
arg.accept(this, `__arg${index}`)
|
|
133
|
-
) as
|
|
239
|
+
) as FieldNode
|
|
134
240
|
})
|
|
135
241
|
|
|
136
242
|
const defaultValues = fields.map((field) => field.defaultValue)
|
|
@@ -171,15 +277,15 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
171
277
|
label: string
|
|
172
278
|
): RecordField {
|
|
173
279
|
const name = this.currentName()
|
|
174
|
-
const fields:
|
|
175
|
-
const fieldMap = new Map<string,
|
|
280
|
+
const fields: FieldNode[] = []
|
|
281
|
+
const fieldMap = new Map<string, FieldNode>()
|
|
176
282
|
const defaultValue: Record<string, unknown> = {}
|
|
177
283
|
const schemaShape: Record<string, z.ZodTypeAny> = {}
|
|
178
284
|
|
|
179
285
|
for (const [key, type] of fields_) {
|
|
180
286
|
const field = this.withName(name ? `.${key}` : key, () =>
|
|
181
287
|
type.accept(this, key)
|
|
182
|
-
) as
|
|
288
|
+
) as FieldNode
|
|
183
289
|
|
|
184
290
|
fields.push(field)
|
|
185
291
|
fieldMap.set(key, field)
|
|
@@ -192,7 +298,10 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
192
298
|
return {
|
|
193
299
|
type: "record",
|
|
194
300
|
label,
|
|
301
|
+
displayLabel: formatLabel(label),
|
|
195
302
|
name,
|
|
303
|
+
component: "record-container",
|
|
304
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
196
305
|
fields,
|
|
197
306
|
fieldMap,
|
|
198
307
|
defaultValue,
|
|
@@ -207,29 +316,44 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
207
316
|
label: string
|
|
208
317
|
): VariantField {
|
|
209
318
|
const name = this.currentName()
|
|
210
|
-
const fields:
|
|
319
|
+
const fields: FieldNode[] = []
|
|
211
320
|
const options: string[] = []
|
|
212
|
-
const optionMap = new Map<string,
|
|
321
|
+
const optionMap = new Map<string, FieldNode>()
|
|
213
322
|
const variantSchemas: z.ZodTypeAny[] = []
|
|
214
323
|
|
|
215
324
|
for (const [key, type] of fields_) {
|
|
216
325
|
const field = this.withName(`.${key}`, () =>
|
|
217
326
|
type.accept(this, key)
|
|
218
|
-
) as
|
|
327
|
+
) as FieldNode
|
|
219
328
|
|
|
220
329
|
fields.push(field)
|
|
221
330
|
options.push(key)
|
|
222
331
|
optionMap.set(key, field)
|
|
223
|
-
|
|
332
|
+
|
|
333
|
+
if (field.type === "null") {
|
|
334
|
+
variantSchemas.push(z.object({ _type: z.literal(key) }))
|
|
335
|
+
} else {
|
|
336
|
+
variantSchemas.push(
|
|
337
|
+
z.object({
|
|
338
|
+
_type: z.literal(key),
|
|
339
|
+
[key]: field.schema,
|
|
340
|
+
})
|
|
341
|
+
)
|
|
342
|
+
}
|
|
224
343
|
}
|
|
225
344
|
|
|
226
345
|
const defaultOption = options[0]
|
|
227
346
|
const firstField = fields[0]
|
|
228
|
-
const defaultValue = {
|
|
229
|
-
[defaultOption]: firstField.defaultValue,
|
|
230
|
-
}
|
|
231
347
|
|
|
232
|
-
const
|
|
348
|
+
const defaultValue =
|
|
349
|
+
firstField.type === "null"
|
|
350
|
+
? { _type: defaultOption }
|
|
351
|
+
: {
|
|
352
|
+
_type: defaultOption,
|
|
353
|
+
[defaultOption]: firstField.defaultValue,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const schema = z.union(variantSchemas)
|
|
233
357
|
|
|
234
358
|
// Helper to get default value for any option
|
|
235
359
|
const getOptionDefault = (option: string): Record<string, unknown> => {
|
|
@@ -237,13 +361,42 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
237
361
|
if (!optField) {
|
|
238
362
|
throw new Error(`Unknown variant option: ${option}`)
|
|
239
363
|
}
|
|
240
|
-
return
|
|
364
|
+
return optField.type === "null"
|
|
365
|
+
? { _type: option }
|
|
366
|
+
: { _type: option, [option]: optField.defaultValue }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Helper to get field for a specific option
|
|
370
|
+
const getField = (option: string): FieldNode => {
|
|
371
|
+
const optField = optionMap.get(option)
|
|
372
|
+
if (!optField) {
|
|
373
|
+
throw new Error(`Unknown variant option: ${option}`)
|
|
374
|
+
}
|
|
375
|
+
return optField
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Helper to get currently selected option from a value
|
|
379
|
+
const getSelectedOption = (value: Record<string, unknown>): string => {
|
|
380
|
+
if (value._type && typeof value._type === "string") {
|
|
381
|
+
return value._type
|
|
382
|
+
}
|
|
383
|
+
const validKeys = Object.keys(value).filter((k) => options.includes(k))
|
|
384
|
+
return validKeys[0] ?? defaultOption
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Helper to get selected field from a value
|
|
388
|
+
const getSelectedField = (value: Record<string, unknown>): FieldNode => {
|
|
389
|
+
const selectedOption = getSelectedOption(value)
|
|
390
|
+
return getField(selectedOption)
|
|
241
391
|
}
|
|
242
392
|
|
|
243
393
|
return {
|
|
244
394
|
type: "variant",
|
|
245
395
|
label,
|
|
396
|
+
displayLabel: formatLabel(label),
|
|
246
397
|
name,
|
|
398
|
+
component: "variant-select",
|
|
399
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
247
400
|
fields,
|
|
248
401
|
options,
|
|
249
402
|
defaultOption,
|
|
@@ -251,6 +404,9 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
251
404
|
defaultValue,
|
|
252
405
|
schema,
|
|
253
406
|
getOptionDefault,
|
|
407
|
+
getField,
|
|
408
|
+
getSelectedOption,
|
|
409
|
+
getSelectedField,
|
|
254
410
|
candidType: "variant",
|
|
255
411
|
}
|
|
256
412
|
}
|
|
@@ -261,7 +417,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
261
417
|
label: string
|
|
262
418
|
): TupleField {
|
|
263
419
|
const name = this.currentName()
|
|
264
|
-
const fields:
|
|
420
|
+
const fields: FieldNode[] = []
|
|
265
421
|
const defaultValue: unknown[] = []
|
|
266
422
|
const schemas: z.ZodTypeAny[] = []
|
|
267
423
|
|
|
@@ -269,7 +425,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
269
425
|
const type = components[index]
|
|
270
426
|
const field = this.withName(`[${index}]`, () =>
|
|
271
427
|
type.accept(this, `_${index}_`)
|
|
272
|
-
) as
|
|
428
|
+
) as FieldNode
|
|
273
429
|
|
|
274
430
|
fields.push(field)
|
|
275
431
|
defaultValue.push(field.defaultValue)
|
|
@@ -281,7 +437,10 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
281
437
|
return {
|
|
282
438
|
type: "tuple",
|
|
283
439
|
label,
|
|
440
|
+
displayLabel: formatLabel(label),
|
|
284
441
|
name,
|
|
442
|
+
component: "tuple-container",
|
|
443
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
285
444
|
fields,
|
|
286
445
|
defaultValue,
|
|
287
446
|
schema,
|
|
@@ -298,7 +457,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
298
457
|
|
|
299
458
|
// For optional, the inner field keeps the same name path
|
|
300
459
|
// because the value replaces null directly (not nested)
|
|
301
|
-
const innerField = ty.accept(this, label) as
|
|
460
|
+
const innerField = ty.accept(this, label) as FieldNode
|
|
302
461
|
|
|
303
462
|
const schema = z.union([
|
|
304
463
|
innerField.schema,
|
|
@@ -310,14 +469,23 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
310
469
|
// Helper to get the inner default when enabling the optional
|
|
311
470
|
const getInnerDefault = (): unknown => innerField.defaultValue
|
|
312
471
|
|
|
472
|
+
// Helper to check if a value represents an enabled optional
|
|
473
|
+
const isEnabled = (value: unknown): boolean => {
|
|
474
|
+
return value !== null && typeof value !== "undefined"
|
|
475
|
+
}
|
|
476
|
+
|
|
313
477
|
return {
|
|
314
478
|
type: "optional",
|
|
315
479
|
label,
|
|
480
|
+
displayLabel: formatLabel(label),
|
|
316
481
|
name,
|
|
482
|
+
component: "optional-toggle",
|
|
483
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
317
484
|
innerField,
|
|
318
485
|
defaultValue: null,
|
|
319
486
|
schema,
|
|
320
487
|
getInnerDefault,
|
|
488
|
+
isEnabled,
|
|
321
489
|
candidType: "opt",
|
|
322
490
|
}
|
|
323
491
|
}
|
|
@@ -335,7 +503,7 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
335
503
|
// Item field uses [0] as template path
|
|
336
504
|
const itemField = this.withName("[0]", () =>
|
|
337
505
|
ty.accept(this, `${label}_item`)
|
|
338
|
-
) as
|
|
506
|
+
) as FieldNode
|
|
339
507
|
|
|
340
508
|
if (isBlob) {
|
|
341
509
|
const schema = z.union([
|
|
@@ -343,14 +511,24 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
343
511
|
z.array(z.number()),
|
|
344
512
|
z.instanceof(Uint8Array),
|
|
345
513
|
])
|
|
514
|
+
|
|
515
|
+
const limits = { ...DEFAULT_BLOB_LIMITS }
|
|
516
|
+
|
|
346
517
|
return {
|
|
347
518
|
type: "blob",
|
|
348
519
|
label,
|
|
520
|
+
displayLabel: formatLabel(label),
|
|
349
521
|
name,
|
|
522
|
+
component: "blob-upload",
|
|
523
|
+
renderHint: FILE_RENDER_HINT,
|
|
350
524
|
itemField,
|
|
351
525
|
defaultValue: "",
|
|
352
526
|
schema,
|
|
353
527
|
acceptedFormats: ["hex", "base64", "file"],
|
|
528
|
+
limits,
|
|
529
|
+
normalizeHex,
|
|
530
|
+
validateInput: (value: string | Uint8Array) =>
|
|
531
|
+
validateBlobInput(value, limits),
|
|
354
532
|
candidType: "blob",
|
|
355
533
|
}
|
|
356
534
|
}
|
|
@@ -360,14 +538,35 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
360
538
|
// Helper to get a new item with default values
|
|
361
539
|
const getItemDefault = (): unknown => itemField.defaultValue
|
|
362
540
|
|
|
541
|
+
// Helper to create an item field for a specific index
|
|
542
|
+
const createItemField = (
|
|
543
|
+
index: number,
|
|
544
|
+
overrides?: { label?: string }
|
|
545
|
+
): FieldNode => {
|
|
546
|
+
// Replace [0] in template with actual index
|
|
547
|
+
const itemName = name ? `${name}[${index}]` : `[${index}]`
|
|
548
|
+
const itemLabel = overrides?.label ?? `Item ${index}`
|
|
549
|
+
|
|
550
|
+
return {
|
|
551
|
+
...itemField,
|
|
552
|
+
name: itemName,
|
|
553
|
+
label: itemLabel,
|
|
554
|
+
displayLabel: formatLabel(itemLabel),
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
363
558
|
return {
|
|
364
559
|
type: "vector",
|
|
365
560
|
label,
|
|
561
|
+
displayLabel: formatLabel(label),
|
|
366
562
|
name,
|
|
563
|
+
component: "vector-list",
|
|
564
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
367
565
|
itemField,
|
|
368
566
|
defaultValue: [],
|
|
369
567
|
schema,
|
|
370
568
|
getItemDefault,
|
|
569
|
+
createItemField,
|
|
371
570
|
candidType: "vec",
|
|
372
571
|
}
|
|
373
572
|
}
|
|
@@ -385,13 +584,13 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
385
584
|
if (this.recursiveSchemas.has(typeName)) {
|
|
386
585
|
schema = this.recursiveSchemas.get(typeName)!
|
|
387
586
|
} else {
|
|
388
|
-
schema = z.lazy(() => (ty.accept(this, label) as
|
|
587
|
+
schema = z.lazy(() => (ty.accept(this, label) as FieldNode).schema)
|
|
389
588
|
this.recursiveSchemas.set(typeName, schema)
|
|
390
589
|
}
|
|
391
590
|
|
|
392
591
|
// Lazy extraction to prevent infinite loops
|
|
393
|
-
const extract = ():
|
|
394
|
-
this.withName(name, () => ty.accept(this, label)) as
|
|
592
|
+
const extract = (): FieldNode =>
|
|
593
|
+
this.withName(name, () => ty.accept(this, label)) as FieldNode
|
|
395
594
|
|
|
396
595
|
// Helper to get inner default (evaluates lazily)
|
|
397
596
|
const getInnerDefault = (): unknown => extract().defaultValue
|
|
@@ -399,7 +598,10 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
399
598
|
return {
|
|
400
599
|
type: "recursive",
|
|
401
600
|
label,
|
|
601
|
+
displayLabel: formatLabel(label),
|
|
402
602
|
name,
|
|
603
|
+
component: "recursive-lazy",
|
|
604
|
+
renderHint: COMPOUND_RENDER_HINT,
|
|
403
605
|
typeName,
|
|
404
606
|
extract,
|
|
405
607
|
defaultValue: undefined,
|
|
@@ -432,42 +634,167 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
432
634
|
}
|
|
433
635
|
)
|
|
434
636
|
|
|
637
|
+
const inputProps: PrimitiveInputProps = {
|
|
638
|
+
type: "text",
|
|
639
|
+
placeholder: "aaaaa-aa or full principal ID",
|
|
640
|
+
minLength: 7,
|
|
641
|
+
maxLength: 64,
|
|
642
|
+
spellCheck: false,
|
|
643
|
+
autoComplete: "off",
|
|
644
|
+
}
|
|
645
|
+
|
|
435
646
|
return {
|
|
436
647
|
type: "principal",
|
|
437
648
|
label,
|
|
649
|
+
displayLabel: formatLabel(label),
|
|
438
650
|
name: this.currentName(),
|
|
651
|
+
component: "principal-input",
|
|
652
|
+
renderHint: TEXT_RENDER_HINT,
|
|
439
653
|
defaultValue: "",
|
|
440
654
|
maxLength: 64,
|
|
441
655
|
minLength: 7,
|
|
656
|
+
format: checkTextFormat(label) as TextFormat,
|
|
442
657
|
schema,
|
|
658
|
+
inputProps,
|
|
443
659
|
candidType: "principal",
|
|
444
|
-
ui: {
|
|
445
|
-
placeholder: "aaaaa-aa or full principal ID",
|
|
446
|
-
},
|
|
447
660
|
}
|
|
448
661
|
}
|
|
449
662
|
|
|
450
663
|
public visitText(_t: IDL.TextClass, label: string): TextField {
|
|
664
|
+
const format = checkTextFormat(label) as TextFormat
|
|
665
|
+
|
|
666
|
+
// Generate format-specific inputProps
|
|
667
|
+
const inputProps = this.getTextInputProps(format)
|
|
668
|
+
|
|
669
|
+
// Generate format-specific schema
|
|
670
|
+
const schema = this.getTextSchema(format)
|
|
671
|
+
|
|
451
672
|
return {
|
|
452
673
|
type: "text",
|
|
453
674
|
label,
|
|
675
|
+
displayLabel: formatLabel(label),
|
|
454
676
|
name: this.currentName(),
|
|
677
|
+
component: "text-input",
|
|
678
|
+
renderHint: TEXT_RENDER_HINT,
|
|
455
679
|
defaultValue: "",
|
|
456
|
-
|
|
680
|
+
format,
|
|
681
|
+
schema,
|
|
682
|
+
inputProps,
|
|
457
683
|
candidType: "text",
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Generate format-specific input props for text fields.
|
|
689
|
+
*/
|
|
690
|
+
private getTextInputProps(format: TextFormat): PrimitiveInputProps {
|
|
691
|
+
switch (format) {
|
|
692
|
+
case "email":
|
|
693
|
+
return {
|
|
694
|
+
type: "email",
|
|
695
|
+
placeholder: "email@example.com",
|
|
696
|
+
inputMode: "email",
|
|
697
|
+
autoComplete: "email",
|
|
698
|
+
spellCheck: false,
|
|
699
|
+
}
|
|
700
|
+
case "url":
|
|
701
|
+
return {
|
|
702
|
+
type: "url",
|
|
703
|
+
placeholder: "https://example.com",
|
|
704
|
+
inputMode: "url",
|
|
705
|
+
autoComplete: "url",
|
|
706
|
+
spellCheck: false,
|
|
707
|
+
}
|
|
708
|
+
case "phone":
|
|
709
|
+
return {
|
|
710
|
+
type: "tel",
|
|
711
|
+
placeholder: "+1 (555) 123-4567",
|
|
712
|
+
inputMode: "tel",
|
|
713
|
+
autoComplete: "tel",
|
|
714
|
+
spellCheck: false,
|
|
715
|
+
}
|
|
716
|
+
case "uuid":
|
|
717
|
+
return {
|
|
718
|
+
type: "text",
|
|
719
|
+
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
720
|
+
pattern:
|
|
721
|
+
"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
|
|
722
|
+
spellCheck: false,
|
|
723
|
+
autoComplete: "off",
|
|
724
|
+
}
|
|
725
|
+
case "btc":
|
|
726
|
+
return {
|
|
727
|
+
type: "text",
|
|
728
|
+
placeholder: "bc1... or 1... or 3...",
|
|
729
|
+
spellCheck: false,
|
|
730
|
+
autoComplete: "off",
|
|
731
|
+
}
|
|
732
|
+
case "eth":
|
|
733
|
+
return {
|
|
734
|
+
type: "text",
|
|
735
|
+
placeholder: "0x...",
|
|
736
|
+
pattern: "0x[0-9a-fA-F]{40}",
|
|
737
|
+
spellCheck: false,
|
|
738
|
+
autoComplete: "off",
|
|
739
|
+
}
|
|
740
|
+
case "account-id":
|
|
741
|
+
return {
|
|
742
|
+
type: "text",
|
|
743
|
+
placeholder: "64-character hex string",
|
|
744
|
+
pattern: "[0-9a-fA-F]{64}",
|
|
745
|
+
maxLength: 64,
|
|
746
|
+
spellCheck: false,
|
|
747
|
+
autoComplete: "off",
|
|
748
|
+
}
|
|
749
|
+
case "principal":
|
|
750
|
+
return {
|
|
751
|
+
type: "text",
|
|
752
|
+
placeholder: "aaaaa-aa or full principal ID",
|
|
753
|
+
minLength: 7,
|
|
754
|
+
maxLength: 64,
|
|
755
|
+
spellCheck: false,
|
|
756
|
+
autoComplete: "off",
|
|
757
|
+
}
|
|
758
|
+
default:
|
|
759
|
+
return {
|
|
760
|
+
type: "text",
|
|
761
|
+
placeholder: "Enter text...",
|
|
762
|
+
spellCheck: true,
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Generate format-specific zod schema for text fields.
|
|
769
|
+
*/
|
|
770
|
+
private getTextSchema(format: TextFormat): z.ZodTypeAny {
|
|
771
|
+
switch (format) {
|
|
772
|
+
case "email":
|
|
773
|
+
return z.email("Invalid email address")
|
|
774
|
+
case "url":
|
|
775
|
+
return z.url("Invalid URL")
|
|
776
|
+
case "uuid":
|
|
777
|
+
return z.uuid("Invalid UUID")
|
|
778
|
+
default:
|
|
779
|
+
return z.string().min(1, "Required")
|
|
461
780
|
}
|
|
462
781
|
}
|
|
463
782
|
|
|
464
783
|
public visitBool(_t: IDL.BoolClass, label: string): BooleanField {
|
|
784
|
+
const inputProps: PrimitiveInputProps = {
|
|
785
|
+
type: "checkbox",
|
|
786
|
+
}
|
|
787
|
+
|
|
465
788
|
return {
|
|
466
789
|
type: "boolean",
|
|
467
790
|
label,
|
|
791
|
+
displayLabel: formatLabel(label),
|
|
468
792
|
name: this.currentName(),
|
|
793
|
+
component: "boolean-checkbox",
|
|
794
|
+
renderHint: CHECKBOX_RENDER_HINT,
|
|
469
795
|
defaultValue: false,
|
|
470
796
|
schema: z.boolean(),
|
|
797
|
+
inputProps,
|
|
471
798
|
candidType: "bool",
|
|
472
799
|
}
|
|
473
800
|
}
|
|
@@ -476,7 +803,13 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
476
803
|
return {
|
|
477
804
|
type: "null",
|
|
478
805
|
label,
|
|
806
|
+
displayLabel: formatLabel(label),
|
|
479
807
|
name: this.currentName(),
|
|
808
|
+
component: "null-hidden",
|
|
809
|
+
renderHint: {
|
|
810
|
+
isCompound: false,
|
|
811
|
+
isPrimitive: true,
|
|
812
|
+
},
|
|
480
813
|
defaultValue: null,
|
|
481
814
|
schema: z.null(),
|
|
482
815
|
candidType: "null",
|
|
@@ -498,6 +831,8 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
498
831
|
max?: string
|
|
499
832
|
}
|
|
500
833
|
): NumberField | TextField {
|
|
834
|
+
const format = checkNumberFormat(label) as NumberFormat
|
|
835
|
+
|
|
501
836
|
let schema = z.string().min(1, "Required")
|
|
502
837
|
|
|
503
838
|
if (options.isFloat) {
|
|
@@ -514,30 +849,57 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
514
849
|
const type = isBigInt ? "text" : "number"
|
|
515
850
|
|
|
516
851
|
if (type === "text") {
|
|
852
|
+
// Propagate timestamp/cycle format if detected, otherwise default to plain
|
|
853
|
+
let textFormat: TextFormat = "plain"
|
|
854
|
+
if (format === "timestamp") textFormat = "timestamp"
|
|
855
|
+
if (format === "cycle") textFormat = "cycle"
|
|
856
|
+
|
|
857
|
+
const inputProps: PrimitiveInputProps = {
|
|
858
|
+
type: "text",
|
|
859
|
+
placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
|
|
860
|
+
inputMode: "numeric",
|
|
861
|
+
pattern: options.unsigned ? "\\d+" : "-?\\d+",
|
|
862
|
+
spellCheck: false,
|
|
863
|
+
autoComplete: "off",
|
|
864
|
+
}
|
|
865
|
+
|
|
517
866
|
return {
|
|
518
867
|
type: "text",
|
|
519
868
|
label,
|
|
869
|
+
displayLabel: formatLabel(label),
|
|
520
870
|
name: this.currentName(),
|
|
871
|
+
component: "text-input",
|
|
872
|
+
renderHint: TEXT_RENDER_HINT,
|
|
521
873
|
defaultValue: "",
|
|
874
|
+
format: textFormat,
|
|
522
875
|
candidType,
|
|
523
876
|
schema,
|
|
524
|
-
|
|
525
|
-
placeholder: options.unsigned ? "e.g. 100000" : "e.g. -100000",
|
|
526
|
-
},
|
|
877
|
+
inputProps,
|
|
527
878
|
}
|
|
528
879
|
}
|
|
529
880
|
|
|
881
|
+
const inputProps: PrimitiveInputProps = {
|
|
882
|
+
type: "number",
|
|
883
|
+
placeholder: options.isFloat ? "0.0" : "0",
|
|
884
|
+
inputMode: options.isFloat ? "decimal" : "numeric",
|
|
885
|
+
min: options.min,
|
|
886
|
+
max: options.max,
|
|
887
|
+
step: options.isFloat ? "any" : "1",
|
|
888
|
+
}
|
|
889
|
+
|
|
530
890
|
return {
|
|
531
891
|
type: "number",
|
|
532
892
|
label,
|
|
893
|
+
displayLabel: formatLabel(label),
|
|
533
894
|
name: this.currentName(),
|
|
895
|
+
component: "number-input",
|
|
896
|
+
renderHint: NUMBER_RENDER_HINT,
|
|
534
897
|
defaultValue: "",
|
|
535
898
|
candidType,
|
|
536
|
-
|
|
899
|
+
format,
|
|
900
|
+
schema,
|
|
901
|
+
inputProps,
|
|
537
902
|
...options,
|
|
538
|
-
ui: {
|
|
539
|
-
placeholder: options.isFloat ? "0.0" : "0",
|
|
540
|
-
},
|
|
541
903
|
}
|
|
542
904
|
}
|
|
543
905
|
|
|
@@ -602,8 +964,15 @@ export class ArgumentFieldVisitor<A = BaseActor> extends IDL.Visitor<
|
|
|
602
964
|
return {
|
|
603
965
|
type: "unknown",
|
|
604
966
|
label,
|
|
967
|
+
displayLabel: formatLabel(label),
|
|
605
968
|
name: this.currentName(),
|
|
969
|
+
component: "unknown-fallback",
|
|
970
|
+
renderHint: {
|
|
971
|
+
isCompound: false,
|
|
972
|
+
isPrimitive: false,
|
|
973
|
+
},
|
|
606
974
|
defaultValue: undefined,
|
|
975
|
+
candidType: "unknown",
|
|
607
976
|
schema: z.any(),
|
|
608
977
|
}
|
|
609
978
|
}
|