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 setResult(await action(...args, ...additionalArgs)); 113 }, 114 [args, additionalArgs] 115 ); 116 117 return ( 118 <> 119 <HeadingText>{name}</HeadingText> 120 <Configurator parameters={parameters} onChange={updateArgument} value={args} /> 121 {additionalParameters.length > 0 && ( 122 <> 123 <Divider text="ADDITIONAL PARAMETERS" /> 124 <Configurator 125 parameters={additionalParameters} 126 onChange={updateAdditionalArgs} 127 value={additionalArgs} 128 /> 129 </> 130 )} 131 <View style={styles.container}> 132 <FunctionSignature namespace={namespace} name={name} parameters={parameters} args={args} /> 133 <View style={styles.buttonsContainer}> 134 {actionsList.map(({ name, action }) => ( 135 <ActionButton key={name} name={name} action={action} onPress={handlePress} /> 136 ))} 137 </View> 138 </View> 139 {result && ( 140 <> 141 <MonoTextWithCountdown onCountdownEnded={() => setResult(undefined)}> 142 {resultToString(result)} 143 </MonoTextWithCountdown> 144 {renderAdditionalResult?.(result)} 145 </> 146 )} 147 </> 148 ); 149} 150 151function initialArgumentFromParameter(parameter: PrimitiveParameter | ConstantParameter) { 152 switch (parameter.type) { 153 case 'boolean': 154 return parameter.initial; 155 case 'string': 156 case 'number': 157 return parameter.values[0]; 158 case 'enum': 159 return parameter.values[0].value; 160 case 'constant': 161 return parameter.value; 162 } 163} 164 165function initialArgumentsFromParameters(parameters: FunctionParameter[]) { 166 return parameters.map((parameter) => { 167 switch (parameter.type) { 168 case 'object': 169 return Object.fromEntries( 170 parameter.properties.map((property) => { 171 return [property.name, initialArgumentFromParameter(property)]; 172 }) 173 ); 174 default: 175 return initialArgumentFromParameter(parameter); 176 } 177 }); 178} 179 180/** 181 * Hook that handles function arguments' values. 182 * Initial value is constructed based on the description of each parameter. 183 */ 184function useArguments( 185 parameters: FunctionParameter[] 186): [FunctionArgument[], OnArgumentChangeCallback] { 187 const [args, setArgs] = useState(initialArgumentsFromParameters(parameters)); 188 const updateArgument = useCallback( 189 (name: ArgumentName, newValue: PrimitiveArgument) => { 190 const parameterIsObject = typeof name === 'object'; 191 const argumentName = parameterIsObject ? name[0] : name; 192 const argumentIdx = parameters.findIndex((parameter) => parameter.name === argumentName); 193 setArgs((currentArgs) => { 194 const newArgs = [...currentArgs]; 195 newArgs[argumentIdx] = parameterIsObject 196 ? { 197 ...(currentArgs[argumentIdx] as object), 198 [name[1]]: newValue, 199 } 200 : newValue; 201 return newArgs; 202 }); 203 }, 204 [parameters] 205 ); 206 return [args, updateArgument]; 207} 208 209function resultToString(result: unknown) { 210 if (result === null) { 211 return 'null'; 212 } 213 214 if (result === 'undefined') { 215 return 'undefined'; 216 } 217 218 if (typeof result === 'object') { 219 const trimmedResult = Object.fromEntries( 220 Object.entries(result).map(([key, value]) => [ 221 key, 222 typeof value === 'string' && value.length > STRING_TRIM_THRESHOLD 223 ? `${value.substring(0, STRING_TRIM_THRESHOLD)}...` 224 : value, 225 ]) 226 ); 227 228 return JSON.stringify(trimmedResult, null, 2); 229 } 230 231 return String(result).length > STRING_TRIM_THRESHOLD 232 ? `${String(result).substring(0, STRING_TRIM_THRESHOLD)}...` 233 : String(result); 234} 235 236const styles = StyleSheet.create({ 237 container: { 238 position: 'relative', 239 paddingBottom: 20, 240 }, 241 buttonsContainer: { 242 position: 'absolute', 243 right: 0, 244 bottom: 3, 245 flexDirection: 'row', 246 }, 247}); 248