1import React, { useState, useCallback, useMemo } from 'react'; 2import { StyleSheet, View } from 'react-native'; 3 4import HeadingText from '../HeadingText'; 5import MonoTextWithCountdown from '../MonoTextWithCountdown'; 6import ActionButton from './ActionButton'; 7import Configurator from './Configurator'; 8import Divider from './Divider'; 9import FunctionSignature from './FunctionSignature'; 10import { 11 ActionFunction, 12 ArgumentName, 13 ConstantParameter, 14 FunctionArgument, 15 FunctionParameter, 16 OnArgumentChangeCallback, 17 PrimitiveArgument, 18 PrimitiveParameter, 19} from './index.types'; 20 21const STRING_TRIM_THRESHOLD = 300; 22 23type Props = { 24 /** 25 * Function namespace/scope (e.g. module name). Used in signature rendering. 26 */ 27 namespace: string; 28 /** 29 * Function name. Used in signature rendering. 30 */ 31 name: string; 32 /** 33 * Function-only parameters. Function's arguments are constructed based on these parameters and passed as-is to the actions callbacks. 34 * These should reflect the actual function signature (type of arguments, default values, order, etc.). 35 */ 36 parameters?: FunctionParameter[]; 37 /** 38 * Additional parameters that are directly mappable to the function arguments. 39 * If you need to add some additional logic to the function call you can do it here. 40 * The current value for these parameters is passed to the actions' callbacks as the additional arguments. 41 */ 42 additionalParameters?: PrimitiveParameter[]; 43 /** 44 * Single action or a list of actions that could be called by the user. Each action would be fetched with the arguments constructed from the parameters. 45 */ 46 actions: ActionFunction | { name: string; action: ActionFunction }[]; 47 /** 48 * Rendering function to render some additional components based on the function's result. 49 */ 50 renderAdditionalResult?: (result: unknown) => JSX.Element | void; 51}; 52 53/** 54 * Helper type for typing out the function description that is later passed to the `FunctionDemo` component. 55 */ 56export type FunctionDescription = Omit<Props, 'namespace' | 'renderAdditionalResult'>; 57 58/** 59 * FunctionDemo is a component that allows visualizing the function call. 60 * It also allows the function's arguments manipulation and invoking the the function via the actions prop. 61 * Additionally it presents the result of the successful function call. 62 * 63 * @example 64 * ```tsx 65 * const FUNCTION_DESCRIPTION: FunctionDescription = { 66 * name: 'functionName', 67 * parameters: [ 68 * { name: 'param1', type: 'string', values: ['value1', 'value2'] }, 69 * ... 70 * ], 71 * additionalParameters: [ 72 * { name: 'additionalParameter', type: 'boolean', initial: false }, 73 * ... 74 * ] 75 * actions: [ 76 * { 77 * name: 'actionName', 78 * action: async (param1: string, ..., additionalParameter: boolean, ...) => { 79 * ... 80 * return someObject 81 * } 82 * }, 83 * ... 84 * ] 85 * } 86 * 87 * function DemoComponent() { 88 * return ( 89 * <FunctionDemo namespace="ModuleName" {...FUNCTION_DESCRIPTION} /> 90 * ) 91 * } 92 * ``` 93 */ 94export default function FunctionDemo({ 95 namespace, 96 name, 97 parameters = [], 98 actions, 99 renderAdditionalResult, 100 additionalParameters = [], 101}: Props) { 102 const [result, setResult] = useState<unknown>(undefined); 103 const [args, updateArgument] = useArguments(parameters); 104 const [additionalArgs, updateAdditionalArgs] = useArguments(additionalParameters); 105 const actionsList = useMemo( 106 () => (Array.isArray(actions) ? actions : [{ name: 'RUN ▶️', action: actions }]), 107 [actions] 108 ); 109 110 const handlePress = useCallback( 111 async (action: ActionFunction) => { 112 // force clear the previous result if exists 113 setResult(undefined); 114 const newResult = await action(...args, ...additionalArgs); 115 // undefined is a special value hiding the result box 116 // so we need to replace it with a string 117 setResult(newResult === undefined ? 'undefined' : newResult); 118 }, 119 [args, additionalArgs] 120 ); 121 122 return ( 123 <> 124 <HeadingText>{name}</HeadingText> 125 <Configurator parameters={parameters} onChange={updateArgument} value={args} /> 126 {additionalParameters.length > 0 && ( 127 <> 128 <Divider text="ADDITIONAL PARAMETERS" /> 129 <Configurator 130 parameters={additionalParameters} 131 onChange={updateAdditionalArgs} 132 value={additionalArgs} 133 /> 134 </> 135 )} 136 <View style={styles.container}> 137 <FunctionSignature namespace={namespace} name={name} parameters={parameters} args={args} /> 138 <View style={styles.buttonsContainer}> 139 {actionsList.map(({ name, action }) => ( 140 <ActionButton key={name} name={name} action={action} onPress={handlePress} /> 141 ))} 142 </View> 143 </View> 144 {result !== undefined && ( 145 <> 146 <MonoTextWithCountdown onCountdownEnded={() => setResult(undefined)}> 147 {resultToString(result)} 148 </MonoTextWithCountdown> 149 {renderAdditionalResult?.(result)} 150 </> 151 )} 152 </> 153 ); 154} 155 156function initialArgumentFromParameter(parameter: PrimitiveParameter | ConstantParameter) { 157 switch (parameter.type) { 158 case 'boolean': 159 return parameter.initial; 160 case 'string': 161 case 'number': 162 return parameter.values[0]; 163 case 'enum': 164 return parameter.values[0].value; 165 case 'constant': 166 return parameter.value; 167 } 168} 169 170function initialArgumentsFromParameters(parameters: FunctionParameter[]) { 171 return parameters.map((parameter) => { 172 switch (parameter.type) { 173 case 'object': 174 return Object.fromEntries( 175 parameter.properties.map((property) => { 176 return [property.name, initialArgumentFromParameter(property)]; 177 }) 178 ); 179 default: 180 return initialArgumentFromParameter(parameter); 181 } 182 }); 183} 184 185/** 186 * Hook that handles function arguments' values. 187 * Initial value is constructed based on the description of each parameter. 188 */ 189function useArguments( 190 parameters: FunctionParameter[] 191): [FunctionArgument[], OnArgumentChangeCallback] { 192 const [args, setArgs] = useState(initialArgumentsFromParameters(parameters)); 193 const updateArgument = useCallback( 194 (name: ArgumentName, newValue: PrimitiveArgument) => { 195 const parameterIsObject = typeof name === 'object'; 196 const argumentName = parameterIsObject ? name[0] : name; 197 const argumentIdx = parameters.findIndex((parameter) => parameter.name === argumentName); 198 setArgs((currentArgs) => { 199 const newArgs = [...currentArgs]; 200 newArgs[argumentIdx] = parameterIsObject 201 ? { 202 ...(currentArgs[argumentIdx] as object), 203 [name[1]]: newValue, 204 } 205 : newValue; 206 return newArgs; 207 }); 208 }, 209 [parameters] 210 ); 211 return [args, updateArgument]; 212} 213 214function resultToString(result: unknown) { 215 if (result === null) { 216 return 'null'; 217 } 218 219 if (result === 'undefined') { 220 return 'undefined'; 221 } 222 223 if (typeof result === 'object') { 224 const trimmedResult = Object.fromEntries( 225 Object.entries(result).map(([key, value]) => [ 226 key, 227 typeof value === 'string' && value.length > STRING_TRIM_THRESHOLD 228 ? `${value.substring(0, STRING_TRIM_THRESHOLD)}...` 229 : value, 230 ]) 231 ); 232 233 return JSON.stringify(trimmedResult, null, 2); 234 } 235 236 return String(result).length > STRING_TRIM_THRESHOLD 237 ? `${String(result).substring(0, STRING_TRIM_THRESHOLD)}...` 238 : String(result); 239} 240 241const styles = StyleSheet.create({ 242 container: { 243 position: 'relative', 244 paddingBottom: 20, 245 }, 246 buttonsContainer: { 247 position: 'absolute', 248 right: 0, 249 bottom: 3, 250 flexDirection: 'row', 251 }, 252}); 253