1f94354caSBartłomiej Bukowskiimport React, { useState, useCallback, useMemo } from 'react'; 2f94354caSBartłomiej Bukowskiimport { StyleSheet, View } from 'react-native'; 3f94354caSBartłomiej Bukowski 4f94354caSBartłomiej Bukowskiimport ActionButton from './ActionButton'; 5f94354caSBartłomiej Bukowskiimport Configurator from './Configurator'; 6f94354caSBartłomiej Bukowskiimport Divider from './Divider'; 70111d1deSBartłomiej Bukowskiimport FunctionSignature, { generateFunctionSignature } from './FunctionSignature'; 84d58621dSBartłomiej Klocekimport Platforms from './Platforms'; 9f94354caSBartłomiej Bukowskiimport { 10f94354caSBartłomiej Bukowski ActionFunction, 11f94354caSBartłomiej Bukowski ArgumentName, 12f94354caSBartłomiej Bukowski ConstantParameter, 13f94354caSBartłomiej Bukowski FunctionArgument, 14f94354caSBartłomiej Bukowski FunctionParameter, 15f94354caSBartłomiej Bukowski OnArgumentChangeCallback, 164d58621dSBartłomiej Klocek Platform, 17f94354caSBartłomiej Bukowski PrimitiveArgument, 18f94354caSBartłomiej Bukowski PrimitiveParameter, 19f94354caSBartłomiej Bukowski} from './index.types'; 204d58621dSBartłomiej Klocekimport { isCurrentPlatformSupported } from './utils'; 21*8a424bebSJames Ideimport HeadingText from '../HeadingText'; 22*8a424bebSJames Ideimport MonoTextWithCountdown from '../MonoTextWithCountdown'; 23f94354caSBartłomiej Bukowski 24f94354caSBartłomiej Bukowskiconst STRING_TRIM_THRESHOLD = 300; 25f94354caSBartłomiej Bukowski 26f94354caSBartłomiej Bukowskitype Props = { 27f94354caSBartłomiej Bukowski /** 28f94354caSBartłomiej Bukowski * Function namespace/scope (e.g. module name). Used in signature rendering. 29f94354caSBartłomiej Bukowski */ 30f94354caSBartłomiej Bukowski namespace: string; 31f94354caSBartłomiej Bukowski /** 32f94354caSBartłomiej Bukowski * Function name. Used in signature rendering. 33f94354caSBartłomiej Bukowski */ 34f94354caSBartłomiej Bukowski name: string; 35f94354caSBartłomiej Bukowski /** 364d58621dSBartłomiej Klocek * Supported platforms. Used in signature rendering and to grey-out unavailable functions. 374d58621dSBartłomiej Klocek */ 384d58621dSBartłomiej Klocek platforms?: Platform[]; 394d58621dSBartłomiej Klocek /** 40f94354caSBartłomiej Bukowski * Function-only parameters. Function's arguments are constructed based on these parameters and passed as-is to the actions callbacks. 41f94354caSBartłomiej Bukowski * These should reflect the actual function signature (type of arguments, default values, order, etc.). 42f94354caSBartłomiej Bukowski */ 43f94354caSBartłomiej Bukowski parameters?: FunctionParameter[]; 44f94354caSBartłomiej Bukowski /** 45f94354caSBartłomiej Bukowski * Additional parameters that are directly mappable to the function arguments. 46f94354caSBartłomiej Bukowski * If you need to add some additional logic to the function call you can do it here. 47f94354caSBartłomiej Bukowski * The current value for these parameters is passed to the actions' callbacks as the additional arguments. 48f94354caSBartłomiej Bukowski */ 49f94354caSBartłomiej Bukowski additionalParameters?: PrimitiveParameter[]; 50f94354caSBartłomiej Bukowski /** 51f94354caSBartłomiej Bukowski * 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. 52f94354caSBartłomiej Bukowski */ 53f94354caSBartłomiej Bukowski actions: ActionFunction | { name: string; action: ActionFunction }[]; 54f94354caSBartłomiej Bukowski /** 55f94354caSBartłomiej Bukowski * Rendering function to render some additional components based on the function's result. 56f94354caSBartłomiej Bukowski */ 5710d26b99STomasz Sapeta renderAdditionalResult?: (result: any) => JSX.Element | void; 58f94354caSBartłomiej Bukowski}; 59f94354caSBartłomiej Bukowski 60f94354caSBartłomiej Bukowski/** 61f94354caSBartłomiej Bukowski * Helper type for typing out the function description that is later passed to the `FunctionDemo` component. 62f94354caSBartłomiej Bukowski */ 636aedc028Saleqsioexport type FunctionDescription = Omit<Props, 'namespace'>; 64f94354caSBartłomiej Bukowski 650111d1deSBartłomiej Bukowskitype Result = 660111d1deSBartłomiej Bukowski | { 670111d1deSBartłomiej Bukowski type: 'none'; 680111d1deSBartłomiej Bukowski } 690111d1deSBartłomiej Bukowski | { 700111d1deSBartłomiej Bukowski type: 'error'; 710111d1deSBartłomiej Bukowski error: unknown; 720111d1deSBartłomiej Bukowski } 730111d1deSBartłomiej Bukowski | { 740111d1deSBartłomiej Bukowski type: 'success'; 750111d1deSBartłomiej Bukowski result: unknown; 760111d1deSBartłomiej Bukowski }; 770111d1deSBartłomiej Bukowski 78f94354caSBartłomiej Bukowski/** 79f94354caSBartłomiej Bukowski * FunctionDemo is a component that allows visualizing the function call. 80f94354caSBartłomiej Bukowski * It also allows the function's arguments manipulation and invoking the the function via the actions prop. 81f94354caSBartłomiej Bukowski * Additionally it presents the result of the successful function call. 82f94354caSBartłomiej Bukowski * 83f94354caSBartłomiej Bukowski * @example 84f94354caSBartłomiej Bukowski * ```tsx 85f94354caSBartłomiej Bukowski * const FUNCTION_DESCRIPTION: FunctionDescription = { 86f94354caSBartłomiej Bukowski * name: 'functionName', 87f94354caSBartłomiej Bukowski * parameters: [ 88f94354caSBartłomiej Bukowski * { name: 'param1', type: 'string', values: ['value1', 'value2'] }, 89f94354caSBartłomiej Bukowski * ... 90f94354caSBartłomiej Bukowski * ], 91f94354caSBartłomiej Bukowski * additionalParameters: [ 92f94354caSBartłomiej Bukowski * { name: 'additionalParameter', type: 'boolean', initial: false }, 93f94354caSBartłomiej Bukowski * ... 94f94354caSBartłomiej Bukowski * ] 95f94354caSBartłomiej Bukowski * actions: [ 96f94354caSBartłomiej Bukowski * { 97f94354caSBartłomiej Bukowski * name: 'actionName', 98f94354caSBartłomiej Bukowski * action: async (param1: string, ..., additionalParameter: boolean, ...) => { 99f94354caSBartłomiej Bukowski * ... 100f94354caSBartłomiej Bukowski * return someObject 101f94354caSBartłomiej Bukowski * } 102f94354caSBartłomiej Bukowski * }, 103f94354caSBartłomiej Bukowski * ... 104f94354caSBartłomiej Bukowski * ] 105f94354caSBartłomiej Bukowski * } 106f94354caSBartłomiej Bukowski * 107f94354caSBartłomiej Bukowski * function DemoComponent() { 108f94354caSBartłomiej Bukowski * return ( 109f94354caSBartłomiej Bukowski * <FunctionDemo namespace="ModuleName" {...FUNCTION_DESCRIPTION} /> 110f94354caSBartłomiej Bukowski * ) 111f94354caSBartłomiej Bukowski * } 112f94354caSBartłomiej Bukowski * ``` 113f94354caSBartłomiej Bukowski */ 1144d58621dSBartłomiej Klocekexport default function FunctionDemo({ name, platforms = [], ...contentProps }: Props) { 1154d58621dSBartłomiej Klocek const disabled = !isCurrentPlatformSupported(platforms); 1164d58621dSBartłomiej Klocek 1174d58621dSBartłomiej Klocek return ( 1184d58621dSBartłomiej Klocek <View style={disabled && styles.demoContainerDisabled}> 1194d58621dSBartłomiej Klocek <Platforms 1204d58621dSBartłomiej Klocek platforms={platforms} 1214d58621dSBartłomiej Klocek style={styles.platformBadge} 1224d58621dSBartłomiej Klocek textStyle={styles.platformText} 1234d58621dSBartłomiej Klocek /> 1244d58621dSBartłomiej Klocek <HeadingText style={disabled && styles.headerDisabled}>{name}</HeadingText> 1254d58621dSBartłomiej Klocek {!disabled && <FunctionDemoContent name={name} {...contentProps} />} 1264d58621dSBartłomiej Klocek </View> 1274d58621dSBartłomiej Klocek ); 1284d58621dSBartłomiej Klocek} 1294d58621dSBartłomiej Klocek 1304d58621dSBartłomiej Klocekfunction FunctionDemoContent({ 131f94354caSBartłomiej Bukowski namespace, 132f94354caSBartłomiej Bukowski name, 133f94354caSBartłomiej Bukowski parameters = [], 134f94354caSBartłomiej Bukowski actions, 135f94354caSBartłomiej Bukowski renderAdditionalResult, 136f94354caSBartłomiej Bukowski additionalParameters = [], 137f94354caSBartłomiej Bukowski}: Props) { 1380111d1deSBartłomiej Bukowski const [result, setResult] = useState<Result>({ type: 'none' }); 139f94354caSBartłomiej Bukowski const [args, updateArgument] = useArguments(parameters); 140f94354caSBartłomiej Bukowski const [additionalArgs, updateAdditionalArgs] = useArguments(additionalParameters); 141f94354caSBartłomiej Bukowski const actionsList = useMemo( 142f94354caSBartłomiej Bukowski () => (Array.isArray(actions) ? actions : [{ name: 'RUN ▶️', action: actions }]), 143f94354caSBartłomiej Bukowski [actions] 144f94354caSBartłomiej Bukowski ); 145f94354caSBartłomiej Bukowski 146f94354caSBartłomiej Bukowski const handlePress = useCallback( 147f94354caSBartłomiej Bukowski async (action: ActionFunction) => { 148c2fb1d4cSBartłomiej Klocek // force clear the previous result if exists 1490111d1deSBartłomiej Bukowski setResult({ type: 'none' }); 1500111d1deSBartłomiej Bukowski try { 151c2fb1d4cSBartłomiej Klocek const newResult = await action(...args, ...additionalArgs); 1520111d1deSBartłomiej Bukowski setResult({ type: 'success', result: newResult }); 1530111d1deSBartłomiej Bukowski } catch (e) { 1540111d1deSBartłomiej Bukowski logError(e, generateFunctionSignature({ namespace, name, parameters, args })); 1550111d1deSBartłomiej Bukowski setResult({ type: 'error', error: e }); 1560111d1deSBartłomiej Bukowski } 157f94354caSBartłomiej Bukowski }, 158f94354caSBartłomiej Bukowski [args, additionalArgs] 159f94354caSBartłomiej Bukowski ); 160f94354caSBartłomiej Bukowski 161f94354caSBartłomiej Bukowski return ( 162f94354caSBartłomiej Bukowski <> 163f94354caSBartłomiej Bukowski <Configurator parameters={parameters} onChange={updateArgument} value={args} /> 164f94354caSBartłomiej Bukowski {additionalParameters.length > 0 && ( 165f94354caSBartłomiej Bukowski <> 166f94354caSBartłomiej Bukowski <Divider text="ADDITIONAL PARAMETERS" /> 167f94354caSBartłomiej Bukowski <Configurator 168f94354caSBartłomiej Bukowski parameters={additionalParameters} 169f94354caSBartłomiej Bukowski onChange={updateAdditionalArgs} 170f94354caSBartłomiej Bukowski value={additionalArgs} 171f94354caSBartłomiej Bukowski /> 172f94354caSBartłomiej Bukowski </> 173f94354caSBartłomiej Bukowski )} 174f94354caSBartłomiej Bukowski <View style={styles.container}> 175f94354caSBartłomiej Bukowski <FunctionSignature namespace={namespace} name={name} parameters={parameters} args={args} /> 176f94354caSBartłomiej Bukowski <View style={styles.buttonsContainer}> 177f94354caSBartłomiej Bukowski {actionsList.map(({ name, action }) => ( 178f94354caSBartłomiej Bukowski <ActionButton key={name} name={name} action={action} onPress={handlePress} /> 179f94354caSBartłomiej Bukowski ))} 180f94354caSBartłomiej Bukowski </View> 181f94354caSBartłomiej Bukowski </View> 1820111d1deSBartłomiej Bukowski {result.type === 'success' ? ( 183f94354caSBartłomiej Bukowski <> 1840111d1deSBartłomiej Bukowski <MonoTextWithCountdown onCountdownEnded={() => setResult({ type: 'none' })}> 1850111d1deSBartłomiej Bukowski {resultToString(result.result)} 186f94354caSBartłomiej Bukowski </MonoTextWithCountdown> 1870111d1deSBartłomiej Bukowski {renderAdditionalResult?.(result.result)} 188f94354caSBartłomiej Bukowski </> 1890111d1deSBartłomiej Bukowski ) : result.type === 'error' ? ( 1900111d1deSBartłomiej Bukowski <MonoTextWithCountdown 1910111d1deSBartłomiej Bukowski style={styles.errorResult} 1920111d1deSBartłomiej Bukowski onCountdownEnded={() => setResult({ type: 'none' })}> 1930111d1deSBartłomiej Bukowski {errorToString(result.error)} 1940111d1deSBartłomiej Bukowski </MonoTextWithCountdown> 1950111d1deSBartłomiej Bukowski ) : null} 196f94354caSBartłomiej Bukowski </> 197f94354caSBartłomiej Bukowski ); 198f94354caSBartłomiej Bukowski} 199f94354caSBartłomiej Bukowski 2000111d1deSBartłomiej Bukowskifunction logError(e: unknown, functionSignature: string) { 2010111d1deSBartłomiej Bukowski console.error(` 2020111d1deSBartłomiej Bukowski${e} 2030111d1deSBartłomiej Bukowski 2040111d1deSBartłomiej BukowskiFunction call that failed: 2050111d1deSBartłomiej Bukowski 2060111d1deSBartłomiej Bukowski ${functionSignature.replace(/\n/g, '\n ')} 2070111d1deSBartłomiej Bukowski 2080111d1deSBartłomiej Bukowski `); 2090111d1deSBartłomiej Bukowski} 2100111d1deSBartłomiej Bukowski 211f94354caSBartłomiej Bukowskifunction initialArgumentFromParameter(parameter: PrimitiveParameter | ConstantParameter) { 212f94354caSBartłomiej Bukowski switch (parameter.type) { 213f94354caSBartłomiej Bukowski case 'boolean': 214f94354caSBartłomiej Bukowski return parameter.initial; 215f94354caSBartłomiej Bukowski case 'string': 216f94354caSBartłomiej Bukowski case 'number': 217f94354caSBartłomiej Bukowski return parameter.values[0]; 218f94354caSBartłomiej Bukowski case 'enum': 219f94354caSBartłomiej Bukowski return parameter.values[0].value; 220f94354caSBartłomiej Bukowski case 'constant': 221f94354caSBartłomiej Bukowski return parameter.value; 222f94354caSBartłomiej Bukowski } 223f94354caSBartłomiej Bukowski} 224f94354caSBartłomiej Bukowski 225f94354caSBartłomiej Bukowskifunction initialArgumentsFromParameters(parameters: FunctionParameter[]) { 226f94354caSBartłomiej Bukowski return parameters.map((parameter) => { 227f94354caSBartłomiej Bukowski switch (parameter.type) { 228f94354caSBartłomiej Bukowski case 'object': 229f94354caSBartłomiej Bukowski return Object.fromEntries( 230f94354caSBartłomiej Bukowski parameter.properties.map((property) => { 231f94354caSBartłomiej Bukowski return [property.name, initialArgumentFromParameter(property)]; 232f94354caSBartłomiej Bukowski }) 233f94354caSBartłomiej Bukowski ); 234f94354caSBartłomiej Bukowski default: 235f94354caSBartłomiej Bukowski return initialArgumentFromParameter(parameter); 236f94354caSBartłomiej Bukowski } 237f94354caSBartłomiej Bukowski }); 238f94354caSBartłomiej Bukowski} 239f94354caSBartłomiej Bukowski 240f94354caSBartłomiej Bukowski/** 241f94354caSBartłomiej Bukowski * Hook that handles function arguments' values. 242f94354caSBartłomiej Bukowski * Initial value is constructed based on the description of each parameter. 243f94354caSBartłomiej Bukowski */ 244864abfb4STomasz Sapetaexport function useArguments( 245f94354caSBartłomiej Bukowski parameters: FunctionParameter[] 246f94354caSBartłomiej Bukowski): [FunctionArgument[], OnArgumentChangeCallback] { 247f94354caSBartłomiej Bukowski const [args, setArgs] = useState(initialArgumentsFromParameters(parameters)); 248f94354caSBartłomiej Bukowski const updateArgument = useCallback( 249f94354caSBartłomiej Bukowski (name: ArgumentName, newValue: PrimitiveArgument) => { 250f94354caSBartłomiej Bukowski const parameterIsObject = typeof name === 'object'; 251f94354caSBartłomiej Bukowski const argumentName = parameterIsObject ? name[0] : name; 252f94354caSBartłomiej Bukowski const argumentIdx = parameters.findIndex((parameter) => parameter.name === argumentName); 253f94354caSBartłomiej Bukowski setArgs((currentArgs) => { 254f94354caSBartłomiej Bukowski const newArgs = [...currentArgs]; 255f94354caSBartłomiej Bukowski newArgs[argumentIdx] = parameterIsObject 256f94354caSBartłomiej Bukowski ? { 257f94354caSBartłomiej Bukowski ...(currentArgs[argumentIdx] as object), 258f94354caSBartłomiej Bukowski [name[1]]: newValue, 259f94354caSBartłomiej Bukowski } 260f94354caSBartłomiej Bukowski : newValue; 261f94354caSBartłomiej Bukowski return newArgs; 262f94354caSBartłomiej Bukowski }); 263f94354caSBartłomiej Bukowski }, 264f94354caSBartłomiej Bukowski [parameters] 265f94354caSBartłomiej Bukowski ); 266f94354caSBartłomiej Bukowski return [args, updateArgument]; 267f94354caSBartłomiej Bukowski} 268f94354caSBartłomiej Bukowski 269f94354caSBartłomiej Bukowskifunction resultToString(result: unknown) { 270f94354caSBartłomiej Bukowski if (result === null) { 271f94354caSBartłomiej Bukowski return 'null'; 272f94354caSBartłomiej Bukowski } 273f94354caSBartłomiej Bukowski 274f94354caSBartłomiej Bukowski if (result === 'undefined') { 275f94354caSBartłomiej Bukowski return 'undefined'; 276f94354caSBartłomiej Bukowski } 277f94354caSBartłomiej Bukowski 278f94354caSBartłomiej Bukowski if (typeof result === 'object') { 279f94354caSBartłomiej Bukowski const trimmedResult = Object.fromEntries( 280f94354caSBartłomiej Bukowski Object.entries(result).map(([key, value]) => [ 281f94354caSBartłomiej Bukowski key, 282f94354caSBartłomiej Bukowski typeof value === 'string' && value.length > STRING_TRIM_THRESHOLD 283f94354caSBartłomiej Bukowski ? `${value.substring(0, STRING_TRIM_THRESHOLD)}...` 284f94354caSBartłomiej Bukowski : value, 285f94354caSBartłomiej Bukowski ]) 286f94354caSBartłomiej Bukowski ); 287f94354caSBartłomiej Bukowski 288f94354caSBartłomiej Bukowski return JSON.stringify(trimmedResult, null, 2); 289f94354caSBartłomiej Bukowski } 290f94354caSBartłomiej Bukowski 291f94354caSBartłomiej Bukowski return String(result).length > STRING_TRIM_THRESHOLD 292f94354caSBartłomiej Bukowski ? `${String(result).substring(0, STRING_TRIM_THRESHOLD)}...` 293f94354caSBartłomiej Bukowski : String(result); 294f94354caSBartłomiej Bukowski} 295f94354caSBartłomiej Bukowski 2960111d1deSBartłomiej Bukowskifunction errorToString(error: unknown) { 2970111d1deSBartłomiej Bukowski if (error instanceof Error) { 2980111d1deSBartłomiej Bukowski return `${error.name}: ${error.message}`; 2990111d1deSBartłomiej Bukowski } 3000111d1deSBartłomiej Bukowski return String(error); 3010111d1deSBartłomiej Bukowski} 3020111d1deSBartłomiej Bukowski 303f94354caSBartłomiej Bukowskiconst styles = StyleSheet.create({ 304f94354caSBartłomiej Bukowski container: { 305f94354caSBartłomiej Bukowski position: 'relative', 306f94354caSBartłomiej Bukowski paddingBottom: 20, 307f94354caSBartłomiej Bukowski }, 308f94354caSBartłomiej Bukowski buttonsContainer: { 309f94354caSBartłomiej Bukowski position: 'absolute', 310f94354caSBartłomiej Bukowski right: 0, 311f94354caSBartłomiej Bukowski bottom: 3, 312f94354caSBartłomiej Bukowski flexDirection: 'row', 313f94354caSBartłomiej Bukowski }, 3144d58621dSBartłomiej Klocek platformBadge: { 3154d58621dSBartłomiej Klocek position: 'absolute', 3164d58621dSBartłomiej Klocek top: 5, 3174d58621dSBartłomiej Klocek }, 3184d58621dSBartłomiej Klocek platformText: { 3194d58621dSBartłomiej Klocek fontSize: 10, 3204d58621dSBartłomiej Klocek }, 3214d58621dSBartłomiej Klocek headerDisabled: { 3224d58621dSBartłomiej Klocek textDecorationLine: 'line-through', 3234d58621dSBartłomiej Klocek color: '#999', 3244d58621dSBartłomiej Klocek }, 3254d58621dSBartłomiej Klocek demoContainerDisabled: { 3264d58621dSBartłomiej Klocek marginBottom: 10, 3274d58621dSBartłomiej Klocek }, 3280111d1deSBartłomiej Bukowski errorResult: { 3290111d1deSBartłomiej Bukowski borderColor: 'red', 3300111d1deSBartłomiej Bukowski }, 331f94354caSBartłomiej Bukowski}); 332