Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions frontend/src/components/Simulator/ContractParams.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ onMounted(() => {
:name="paramName"
:placeholder="`${paramType}`"
:label="paramName"
:unionTypes="inputMap.getUnionTypes(paramType)"
/>
</div>
<div
Expand All @@ -119,6 +120,7 @@ onMounted(() => {
:name="paramName"
:placeholder="`${paramType}`"
:label="paramName"
:unionTypes="inputMap.getUnionTypes(paramType)"
/>
</div>
</div>
Expand Down
256 changes: 256 additions & 0 deletions frontend/src/components/global/fields/UnionField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue';
import { useUniqueId } from '@/hooks';
import TextInput from '../inputs/TextInput.vue';
import NumberInput from '../inputs/NumberInput.vue';
import CheckboxInput from '../inputs/CheckboxInput.vue';
import FieldLabel from './FieldLabel.vue';
import { AnyFieldValue } from './AnyFieldValue';

const props = defineProps<{
name: string;
unionTypes: string[];
modelValue?: any;
}>();

const emit = defineEmits<{
'update:modelValue': [value: any];
}>();

const fieldId = useUniqueId('union');
const selectedType = ref<string>('');
const selectedGroup = ref<string>('');
const values = ref<{ [key: string]: any }>({});
const isInternalUpdate = ref<boolean>(false);

const getInputComponent = (type: string) => {
switch (type) {
case 'int':
return NumberInput;
case 'bool':
return CheckboxInput;
default:
return TextInput;
}
};

const displayGroups = computed(() => {
const groups: {
id: string;
types: string[];
label: string;
placeholder: string;
}[] = [];
const processed = new Set<string>();

props.unionTypes.forEach((type) => {
const trimmedType = type.trim();
if (processed.has(trimmedType)) return;

const component = getInputComponent(trimmedType);

if (
component === TextInput &&
['array', 'dict', 'address', 'bytes', 'any'].includes(trimmedType)
) {
const complexTypes = props.unionTypes.filter((t) => {
const tt = t.trim();
return ['array', 'dict', 'address', 'bytes', 'any'].includes(tt);
});

if (complexTypes.length > 0) {
const typeLabels = complexTypes.map((t) => t.trim().toLowerCase());
groups.push({
id: 'complex',
types: complexTypes.map((t) => t.trim()),
label: typeLabels.join(','),
placeholder: typeLabels.join('/'),
});

complexTypes.forEach((t) => processed.add(t.trim()));
}
} else if (!processed.has(trimmedType)) {
groups.push({
id: trimmedType,
types: [trimmedType],
label: trimmedType,
placeholder: trimmedType.toLowerCase(),
});
processed.add(trimmedType);
}
});

return groups;
});

const initializeValues = () => {
if (!props.unionTypes || props.unionTypes.length === 0) {
return;
}
const typeMap: { [key: string]: any } = {
string: '',
int: 0,
bool: false,
address: '',
bytes: '',
array: '',
dict: '',
None: null,
null: null,
any: '',
};

props.unionTypes.forEach((type) => {
const trimmedType = type.trim();
if (!trimmedType) return;
// Only initialize if the value doesn't already exist (preserve user input)
if (!(trimmedType in values.value)) {
values.value[trimmedType] = typeMap[trimmedType] ?? '';
}
});

const groups = displayGroups.value;
if (groups.length > 0) {
selectedGroup.value = groups[0].id;
selectedType.value = groups[0].types[0];
} else {
console.warn('No valid union type groups found');
}
};

// Handle external model value updates
watch(
() => props.modelValue,
(newValue) => {
// Skip if this update is from our own emitValue
if (isInternalUpdate.value) {
isInternalUpdate.value = false;
return;
}

// Skip if the new value matches what we already have for the current type
// This prevents overwriting user input when switching radio buttons
if (newValue !== undefined && newValue instanceof AnyFieldValue) {
const currentValue = values.value[selectedType.value];
if (typeof currentValue === 'string' && currentValue === newValue.value) {
return;
}
}

if (newValue !== undefined && newValue instanceof AnyFieldValue) {
// For strings, unwrap the JSON to get the original value
if (typeof newValue.value === 'string') {
try {
values.value[selectedType.value] = JSON.parse(newValue.value);
} catch {
values.value[selectedType.value] = newValue.value;
}
} else {
values.value[selectedType.value] = newValue.value;
}
}
},
);

const getCurrentValue = () => {
const type = selectedType.value;
const value = values.value[type];

if (!type || value === undefined) {
return undefined;
}

if (type === 'string') {
return new AnyFieldValue(JSON.stringify(value));
}

if (
type === 'array' ||
type === 'dict' ||
type === 'address' ||
type === 'bytes' ||
type === 'any'
) {
return new AnyFieldValue(value);
}

return value;
};

const emitValue = () => {
const currentValue = getCurrentValue();
isInternalUpdate.value = true;
emit('update:modelValue', currentValue);
};

// Initialize and watch for changes
watch(
() => props.unionTypes,
(newTypes) => {
if (newTypes?.length > 0) {
initializeValues();
emitValue();
}
},
{ immediate: true },
);

// Watch for group changes to update selected type
watch(selectedGroup, (newGroup) => {
const groups = displayGroups.value;
const group = groups.find((g) => g.id === newGroup);
if (group && group.types.length > 0) {
selectedType.value = group.types[0];
emitValue();
}
});

// Watch for value changes
watch([selectedType, values], () => {
emitValue();
});
</script>

<template>
<div class="flex w-full flex-col gap-2">
<FieldLabel :for="fieldId" tiny>{{ name }}</FieldLabel>

<div class="flex flex-col gap-2">
<div
v-for="group in displayGroups"
:key="group.id"
class="flex w-full flex-row items-center gap-2"
>
<input
:id="`${fieldId}-radio-${group.id}`"
v-model="selectedGroup"
:value="group.id"
type="radio"
:name="`${fieldId}-group`"
class="text-primary-600 h-4 w-4 border-gray-300 text-primary outline-0 focus:ring-accent dark:border-gray-500 dark:bg-transparent dark:text-accent"
/>

<div
v-if="group.id === 'None' || group.id === 'null'"
class="text-xs text-gray-500"
>
None
</div>

<div v-else class="flex w-full flex-row items-center gap-2">
<component
:is="getInputComponent(group.types[0])"
v-model="values[group.types[0]]"
:placeholder="group.placeholder"
:disabled="selectedGroup !== group.id"
:id="`${fieldId}-${group.id}`"
:name="`${props.name}-${group.id}`"
@update:modelValue="emitValue"
tiny
:class="group.types[0] === 'bool' ? 'h-4 w-4' : 'w-full flex-1'"
/>
</div>
</div>
</div>
</div>
</template>
16 changes: 15 additions & 1 deletion frontend/src/hooks/useInputMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AnyField from '@/components/global/fields/AnyField.vue';
import StringField from '@/components/global/fields/StringField.vue';
import IntegerField from '@/components/global/fields/IntegerField.vue';
import BooleanField from '@/components/global/fields/BooleanField.vue';
import UnionField from '@/components/global/fields/UnionField.vue';
import type { ContractParamsSchema } from 'genlayer-js/types';

export const InputTypesMap: { [k: string]: any } = {
Expand All @@ -13,9 +14,14 @@ export const InputTypesMap: { [k: string]: any } = {

export const useInputMap = () => {
const getComponent = (type: ContractParamsSchema) => {
if (typeof type === 'object' && type !== null && '$or' in type) {
return UnionField;
}

if (typeof type !== 'string') {
type = 'any';
}

const component = InputTypesMap[type];

if (!component) {
Expand All @@ -28,5 +34,13 @@ export const useInputMap = () => {
return component;
};

return { getComponent };
const getUnionTypes = (type: ContractParamsSchema): string[] => {
if (typeof type === 'object' && type !== null && '$or' in type) {
return (type as any).$or || [];
}

return [];
};
Comment on lines +37 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Enhance error handling and type safety.

The getUnionTypes function needs better error handling and type safety for the $or property access.

Apply this diff to improve robustness:

   const getUnionTypes = (type: ContractParamsSchema): string[] => {
     if (typeof type === 'object' && type !== null && '$or' in type) {
-      return (type as any).$or || [];
+      const unionTypes = (type as any).$or;
+      if (Array.isArray(unionTypes)) {
+        return unionTypes.filter(t => typeof t === 'string');
+      }
     }

     return [];
   };
🤖 Prompt for AI Agents
In frontend/src/hooks/useInputMap.ts around lines 37 to 43, the getUnionTypes
function accesses the $or property without ensuring it is an array, risking
runtime errors. Improve type safety by checking if $or exists and is an array
before returning it; otherwise, return an empty array. Add appropriate type
guards or assertions to ensure robust and safe access to the $or property.


return { getComponent, getUnionTypes };
};