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