mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-21 22:57:17 +01:00
feat(connect-explorer-nextra): complex/nested types handling, refactor
This commit is contained in:
committed by
Tomáš Martykán
parent
cb82750924
commit
905756a4a5
@@ -19,6 +19,7 @@
|
||||
"@trezor/connect-web": "workspace:^",
|
||||
"@trezor/connect-webextension": "workspace:^",
|
||||
"@trezor/protobuf": "workspace:^",
|
||||
"@trezor/schema-utils": "workspace:^",
|
||||
"@trezor/utils": "workspace:^",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
|
||||
import TrezorConnect from '@trezor/connect-web';
|
||||
|
||||
import { GetState, Dispatch, Field } from '../types';
|
||||
|
||||
export const SET_METHOD = 'method_set';
|
||||
export const SET_SCHEMA = 'schema_set';
|
||||
export const FIELD_CHANGE = 'method_field_change';
|
||||
export const FIELD_DATA_CHANGE = 'method_field_data_change';
|
||||
export const ADD_BATCH = 'method_add_batch';
|
||||
@@ -11,6 +14,7 @@ export const RESPONSE = 'method_response';
|
||||
|
||||
export type MethodAction =
|
||||
| { type: typeof SET_METHOD; methodConfig: any }
|
||||
| { type: typeof SET_SCHEMA; method: keyof typeof TrezorConnect; schema: TSchema }
|
||||
| { type: typeof FIELD_CHANGE; field: Field<any>; value: any }
|
||||
| { type: typeof FIELD_DATA_CHANGE; field: Field<any>; data: any }
|
||||
| { type: typeof ADD_BATCH; field: Field<any>; item: any }
|
||||
@@ -22,6 +26,12 @@ export const onSetMethod = (methodConfig: any) => ({
|
||||
methodConfig,
|
||||
});
|
||||
|
||||
export const onSetSchema = (method: string, schema: TSchema) => ({
|
||||
type: SET_SCHEMA,
|
||||
method,
|
||||
schema,
|
||||
});
|
||||
|
||||
export const onFieldChange = (field: Field<any>, value: any) => ({
|
||||
type: FIELD_CHANGE,
|
||||
field,
|
||||
@@ -66,6 +76,7 @@ export const onSubmit = () => async (dispatch: Dispatch, getState: GetState) =>
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-expect-error params type is unknown
|
||||
const response = await connectMethod({
|
||||
...method.params,
|
||||
});
|
||||
@@ -84,7 +95,7 @@ export const onVerify = () => (dispatch: Dispatch, getState: GetState) => {
|
||||
message: method.params.message,
|
||||
hex: undefined,
|
||||
publicKey: undefined,
|
||||
};
|
||||
} as any;
|
||||
|
||||
// ethereum extra field
|
||||
if ('hex' in method.params) {
|
||||
|
||||
@@ -45,7 +45,8 @@ export const init =
|
||||
// The event `WEBEXTENSION.CHANNEL_HANDSHAKE_CONFIRM` is coming from @trezor/connect-webextension/proxy
|
||||
// that is replacing @trezor/connect-web when connect-explorer is run in connect-explorer-webextension
|
||||
// so Typescript cannot recognize it.
|
||||
(TrezorConnect.on as any)(WEBEXTENSION.CHANNEL_HANDSHAKE_CONFIRM, event => {
|
||||
// @ts-expect-error
|
||||
TrezorConnect.on(WEBEXTENSION.CHANNEL_HANDSHAKE_CONFIRM, event => {
|
||||
if (event.type === WEBEXTENSION.CHANNEL_HANDSHAKE_CONFIRM) {
|
||||
dispatch({ type: ACTIONS.ON_HANDSHAKE_CONFIRMED });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { Optional, Kind, TSchema } from '@sinclair/typebox';
|
||||
import { TSchema } from '@sinclair/typebox';
|
||||
|
||||
import { Card, CollapsibleBox, variables } from '@trezor/components';
|
||||
|
||||
@@ -33,96 +33,16 @@ const ApiPlaygroundWrapper = styled(Card)`
|
||||
}
|
||||
`;
|
||||
|
||||
const schemaToFields = (schema: TSchema) => {
|
||||
console.log(schema);
|
||||
|
||||
if (schema[Kind] === 'Object') {
|
||||
return Object.keys(schema.properties).flatMap(key => {
|
||||
const field = schema.properties[key];
|
||||
if (field[Kind] === 'Object') {
|
||||
return [
|
||||
{
|
||||
name: key,
|
||||
label: key,
|
||||
type: 'json',
|
||||
value: undefined,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return schemaToFields(field).map(field => {
|
||||
return {
|
||||
...field,
|
||||
name: [key, field.name].filter(v => v).join('.'),
|
||||
};
|
||||
});
|
||||
});
|
||||
} else if (schema[Kind] === 'Array') {
|
||||
const fields = schemaToFields(schema.items);
|
||||
|
||||
return [
|
||||
{
|
||||
name: '',
|
||||
type: 'array',
|
||||
batch: [
|
||||
{
|
||||
type: '',
|
||||
fields,
|
||||
},
|
||||
],
|
||||
items: schema[Optional] === 'Optional' ? [] : [fields],
|
||||
},
|
||||
];
|
||||
} else if (schema[Kind] === 'Intersect') {
|
||||
return schema.allOf?.flatMap(schemaToFields);
|
||||
} else if (schema[Kind] === 'Union') {
|
||||
const onlyLiterals = schema.anyOf?.every(s => s[Kind] === 'Literal');
|
||||
if (onlyLiterals) {
|
||||
const filtered = schema.anyOf?.filter((s, i) => s.const !== i.toString());
|
||||
const options = filtered.length > 0 ? filtered : schema.anyOf;
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'select',
|
||||
value: schema.default,
|
||||
optional: schema[Optional] === 'Optional',
|
||||
data: options.map(s => ({ label: s.const, value: s.const })),
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
String: 'input',
|
||||
Number: 'number',
|
||||
Uint: 'number',
|
||||
Boolean: 'checkbox',
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
type: typeMap[schema[Kind]] ?? 'input',
|
||||
value: schema.default,
|
||||
optional: schema[Optional] === 'Optional',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
interface ApiPlaygroundProps {
|
||||
method: string;
|
||||
schema: TSchema;
|
||||
}
|
||||
export const ApiPlayground = ({ method, schema }: ApiPlaygroundProps) => {
|
||||
const actions = useActions({
|
||||
onSetMethod: methodActions.onSetMethod,
|
||||
onSetSchema: methodActions.onSetSchema,
|
||||
});
|
||||
useEffect(() => {
|
||||
const fields = schemaToFields(schema);
|
||||
actions.onSetMethod({
|
||||
name: method,
|
||||
fields,
|
||||
submitButton: 'Submit',
|
||||
});
|
||||
actions.onSetSchema(method, schema);
|
||||
}, [actions, method, schema]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const copy = data => {
|
||||
const copy = (data?: string) => {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = data;
|
||||
el.value = data ?? '';
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
@@ -33,7 +33,10 @@ const ClipboardButton = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
export const CopyToClipboard = props => (
|
||||
interface CopyToClipboardProps {
|
||||
data?: string;
|
||||
}
|
||||
export const CopyToClipboard = (props: CopyToClipboardProps) => (
|
||||
<ClipboardButton title="Copy to clipboard" onClick={_event => copy(props.data)}>
|
||||
<svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid">
|
||||
<path d="M 5 2 C 3.9 2 3 2.9 3 4 L 3 17 L 5 17 L 5 4 L 15 4 L 15 2 L 5 2 z M 9 6 C 7.9 6 7 6.9 7 8 L 7 20 C 7 21.1 7.9 22 9 22 L 18 22 C 19.1 22 20 21.1 20 20 L 20 8 C 20 6.9 19.1 6 18 6 L 9 6 z M 9 8 L 18 8 L 18 20 L 9 20 L 9 8 z" />
|
||||
|
||||
@@ -24,11 +24,16 @@ const getArray = (field: FieldWithBundle<any>, props: Props) => (
|
||||
>
|
||||
{field.items?.map((batch, index) => {
|
||||
const key = `${field.name}-${index}`;
|
||||
const children = batch.map((batchField: any) => getField(batchField, props));
|
||||
// Move all booleans to the end while not breaking the order of other fields
|
||||
const bools = batch.filter(f => f.type === 'checkbox');
|
||||
const nonBools = batch.filter(f => f.type !== 'checkbox');
|
||||
const boolsChildren = bools.map((batchField: any) => getField(batchField, props));
|
||||
const children = nonBools.map((batchField: any) => getField(batchField, props));
|
||||
|
||||
return (
|
||||
<BatchWrapper key={key} onRemove={() => props.actions.onBatchRemove(field, batch)}>
|
||||
{children}
|
||||
<Checkboxes>{boolsChildren}</Checkboxes>
|
||||
</BatchWrapper>
|
||||
);
|
||||
})}
|
||||
@@ -95,11 +100,11 @@ const Container = styled.div`
|
||||
position: relative;
|
||||
background: ${({ theme }) => theme.backgroundSurfaceElevation2};
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
min-height: 150px;
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@@ -116,6 +121,12 @@ const Heading = styled(H3)`
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const Checkboxes = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 10px;
|
||||
`;
|
||||
|
||||
interface VerifyButtonProps {
|
||||
onClick: (url: string) => void;
|
||||
name: string;
|
||||
@@ -160,9 +171,7 @@ export const Method = () => {
|
||||
<div>
|
||||
<Heading>Params</Heading>
|
||||
{nonBools.map(field => getField(field, { actions }))}
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', padding: '10px 0' }}>
|
||||
{bools.map(field => getField(field, { actions }))}
|
||||
</div>
|
||||
<Checkboxes>{bools.map(field => getField(field, { actions }))}</Checkboxes>
|
||||
<Row>
|
||||
<Button onClick={onSubmit} data-test="@submit-button">
|
||||
{submitButton}
|
||||
|
||||
@@ -6,8 +6,10 @@ import styled from 'styled-components';
|
||||
import { Badge } from '@trezor/components';
|
||||
|
||||
interface ParamProps {
|
||||
id?: string;
|
||||
name: string;
|
||||
type: string | React.ReactNode;
|
||||
typeLink?: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -22,7 +24,7 @@ const ParamWrapper = styled.div`
|
||||
transparent
|
||||
);
|
||||
`;
|
||||
const ParamRow = styled.div`
|
||||
const ParamRow = styled.a`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@@ -41,17 +43,26 @@ const ParamName = styled.h4`
|
||||
font-family: monospace;
|
||||
font-size: 0.875rem;
|
||||
`;
|
||||
const ParamType = styled.div`
|
||||
const ParamType = styled.div<{
|
||||
isLink?: boolean;
|
||||
}>`
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
|
||||
${({ isLink, theme }) =>
|
||||
isLink &&
|
||||
`
|
||||
color: ${theme.TYPE_GREEN};
|
||||
text-decoration: underline;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Param = (props: ParamProps) => {
|
||||
return (
|
||||
<ParamWrapper>
|
||||
<ParamRow>
|
||||
<ParamWrapper id={props.id}>
|
||||
<ParamRow href={props.typeLink}>
|
||||
<ParamName>{props.name}</ParamName>
|
||||
<ParamType>
|
||||
<ParamType isLink={!!props.typeLink}>
|
||||
{typeof props.type === 'string' ? (
|
||||
<Markdown>{props.type}</Markdown>
|
||||
) : (
|
||||
|
||||
@@ -4,10 +4,6 @@ import { Hint, Kind, Optional, TIntersect, TObject, TSchema } from '@sinclair/ty
|
||||
|
||||
import { Param } from './Param';
|
||||
|
||||
type ParamsTableProps = {
|
||||
schema: TObject | TIntersect | TSchema;
|
||||
};
|
||||
|
||||
const descriptionDictionary: Record<string, string> = {
|
||||
path: 'Derivation path',
|
||||
showOnTrezor: 'Display the result on the Trezor device. Default is false',
|
||||
@@ -26,15 +22,23 @@ const descriptionDictionary: Record<string, string> = {
|
||||
const getTypeName = (value: TSchema, hasDescendants?: boolean) => {
|
||||
let typeName = value[Kind];
|
||||
if (value[Kind] === 'Array') {
|
||||
typeName = `Array<${getTypeName(value.items)}>`;
|
||||
const childTypeName = getTypeName(value.items);
|
||||
typeName = childTypeName ? `Array<${childTypeName}>` : 'Array';
|
||||
} else if (value[Kind] === 'Literal') {
|
||||
if (typeof value.const === 'number') {
|
||||
typeName = value.const.toString();
|
||||
} else {
|
||||
typeName = JSON.stringify(value.const);
|
||||
}
|
||||
if (value.$id) {
|
||||
typeName = value.$id + ' (' + typeName + ')';
|
||||
}
|
||||
} else if (value[Kind] === 'Union' && !hasDescendants) {
|
||||
const itemsFiltered = value.anyOf?.filter((v, i) => {
|
||||
const itemsFiltered = value.anyOf?.filter((v: TSchema, i: number) => {
|
||||
// Filter enum non-numbers
|
||||
if (value[Hint] === 'Enum' && v[Kind] === 'Literal' && typeof v.const === 'string') {
|
||||
return false;
|
||||
}
|
||||
// Filter union number indexes - unnecessary to display
|
||||
if (v[Kind] === 'Literal' && (v.const === i || v.const === i.toString())) {
|
||||
return false;
|
||||
@@ -43,13 +47,13 @@ const getTypeName = (value: TSchema, hasDescendants?: boolean) => {
|
||||
return true;
|
||||
});
|
||||
if (itemsFiltered.length > 0) {
|
||||
typeName = itemsFiltered?.map(v => getTypeName(v)).join(' | ');
|
||||
} else if (value[Hint] === 'Enum') {
|
||||
// TODO handle enums - need to add $id in schema-utils
|
||||
typeName = 'Enum';
|
||||
typeName = itemsFiltered?.map((v: TSchema) => getTypeName(v)).join(' | ');
|
||||
}
|
||||
if (value[Hint] === 'Enum') {
|
||||
typeName = 'Enum: ' + typeName;
|
||||
}
|
||||
} else if (value[Kind] === 'Intersect' && !hasDescendants) {
|
||||
typeName = value.anyOf?.map(v => getTypeName(v)).join(' & ');
|
||||
typeName = value.anyOf?.map((v: TSchema) => getTypeName(v)).join(' & ');
|
||||
} else if (value[Kind] === 'Object' && value.$id) {
|
||||
typeName = value.$id;
|
||||
}
|
||||
@@ -61,15 +65,30 @@ interface SingleParamProps {
|
||||
name: string;
|
||||
value: TSchema;
|
||||
schema?: TSchema;
|
||||
topLevelSchema: TObject | TIntersect | TSchema;
|
||||
isTopLevel?: boolean;
|
||||
}
|
||||
const SingleParam = ({ name, value, schema }: SingleParamProps) => {
|
||||
const SingleParam = ({ name, value, schema, topLevelSchema, isTopLevel }: SingleParamProps) => {
|
||||
// Show descendants for complex objects
|
||||
const complexObjects = ['Object', 'Union', 'Intersect'];
|
||||
let hasDescendants = complexObjects.includes(value[Kind]);
|
||||
let typeLink: string | undefined;
|
||||
if (value[Kind] === 'Union') {
|
||||
hasDescendants = value.anyOf.some(v => complexObjects.includes(v[Kind]));
|
||||
// Show descendants for unions only if they contain complex objects
|
||||
hasDescendants = value.anyOf.some((v: TSchema) => complexObjects.includes(v[Kind]));
|
||||
} else if (value[Kind] === 'Array') {
|
||||
// Show descendants for arrays only if they contain complex objects
|
||||
hasDescendants = complexObjects.includes(value.items[Kind]);
|
||||
} else if (
|
||||
value[Kind] === 'Object' &&
|
||||
topLevelSchema?.[Kind] === 'Object' &&
|
||||
!isTopLevel &&
|
||||
value.$id &&
|
||||
Object.entries(topLevelSchema.properties).some(([_, val]: any) => val.$id === value.$id)
|
||||
) {
|
||||
// If the object is a reference to the top level object, don't show descendants
|
||||
hasDescendants = false;
|
||||
typeLink = `#${value.$id}`;
|
||||
}
|
||||
// Get the type name
|
||||
const typeName = getTypeName(value, hasDescendants);
|
||||
@@ -85,39 +104,55 @@ const SingleParam = ({ name, value, schema }: SingleParamProps) => {
|
||||
return (
|
||||
<>
|
||||
<Param
|
||||
id={isTopLevel ? value.$id : undefined}
|
||||
key={name}
|
||||
name={name}
|
||||
type={typeName}
|
||||
typeLink={typeLink}
|
||||
required={isRequired}
|
||||
description={value.description ?? descriptionDictionary[name]}
|
||||
/>
|
||||
{hasDescendants && (
|
||||
<div style={{ marginLeft: 30 }}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-use-before-define */}
|
||||
<ParamsTable schema={value} />
|
||||
<ParamsTable schema={value} topLevelSchema={topLevelSchema} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ParamsTable = ({ schema }: ParamsTableProps) => {
|
||||
type ParamsTableProps = {
|
||||
schema: TObject | TIntersect | TSchema;
|
||||
topLevelSchema?: TObject | TIntersect | TSchema;
|
||||
};
|
||||
export const ParamsTable = ({ schema, topLevelSchema }: ParamsTableProps) => {
|
||||
const topLevelSchemaCurrent = topLevelSchema ?? schema;
|
||||
if (schema[Kind] === 'Union') {
|
||||
return schema.anyOf?.map((param, i) => (
|
||||
return schema.anyOf?.map((param: TSchema, i: number) => (
|
||||
<>
|
||||
{i > 0 && <h3>or</h3>}
|
||||
<ParamsTable key={i} schema={param} />
|
||||
<ParamsTable key={i} schema={param} topLevelSchema={topLevelSchemaCurrent} />
|
||||
</>
|
||||
));
|
||||
} else if (schema[Kind] === 'Intersect') {
|
||||
return schema.allOf?.map((param, i) => <ParamsTable key={i} schema={param} />);
|
||||
return schema.allOf?.map((param: TSchema, i: number) => (
|
||||
<ParamsTable key={i} schema={param} topLevelSchema={topLevelSchemaCurrent} />
|
||||
));
|
||||
} else if (schema[Kind] === 'Object') {
|
||||
return Object.entries(schema.properties)?.map(([name, value]: [string, any]) => (
|
||||
<SingleParam name={name} value={value} schema={schema} key={name} />
|
||||
<SingleParam
|
||||
name={name}
|
||||
value={value}
|
||||
schema={schema}
|
||||
key={name}
|
||||
topLevelSchema={topLevelSchemaCurrent}
|
||||
isTopLevel={topLevelSchema === undefined}
|
||||
/>
|
||||
));
|
||||
} else if (schema[Kind] === 'Array') {
|
||||
return <SingleParam name="" value={schema.items} />;
|
||||
return <SingleParam name="" value={schema.items} topLevelSchema={topLevelSchemaCurrent} />;
|
||||
} else {
|
||||
return <SingleParam name="" value={schema} />;
|
||||
return <SingleParam name="" value={schema} topLevelSchema={topLevelSchemaCurrent} />;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ const AddBatchButton = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
`;
|
||||
|
||||
const AddButton = ({ field, onAdd, label }: AddButtonProps) => {
|
||||
|
||||
@@ -2,22 +2,22 @@ import { ReactNode } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { THEME, Icon } from '@trezor/components';
|
||||
import { Icon, Card } from '@trezor/components';
|
||||
|
||||
interface BatchWrapperProps {
|
||||
children: ReactNode;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
const Wrapper = styled(Card)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid ${THEME.light.STROKE_GREY};
|
||||
margin: 16px;
|
||||
margin: 4px 0 8px 32px;
|
||||
`;
|
||||
|
||||
const Fields = styled.div`
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
export const BatchWrapper = ({ children, onRemove }: BatchWrapperProps) => (
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { Checkbox as CheckboxComponent } from '@trezor/components';
|
||||
import { Card, Checkbox as CheckboxComponent } from '@trezor/components';
|
||||
|
||||
import type { Field } from '../../types';
|
||||
import type { FieldBasic } from '../../types';
|
||||
import { onFieldChange } from '../../actions/methodActions';
|
||||
import { Row } from './Row';
|
||||
|
||||
interface CheckboxProps {
|
||||
field: Field<boolean>;
|
||||
field: FieldBasic<boolean>;
|
||||
onChange: typeof onFieldChange;
|
||||
}
|
||||
|
||||
const Checkbox = ({ field, onChange, ...rest }: CheckboxProps) => (
|
||||
<Row style={{ width: '50%' }}>
|
||||
<CheckboxComponent
|
||||
onClick={_e => onChange(field, !field.value)}
|
||||
isChecked={field.value}
|
||||
{...rest}
|
||||
>
|
||||
{field.name}
|
||||
</CheckboxComponent>
|
||||
<Row>
|
||||
<Card paddingType="small" onClick={() => onChange(field, !field.value)}>
|
||||
<CheckboxComponent onClick={() => {}} isChecked={field.value} {...rest}>
|
||||
{field.name}
|
||||
</CheckboxComponent>
|
||||
</Card>
|
||||
</Row>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Select } from '@trezor/components';
|
||||
|
||||
import type { Field } from '../../types';
|
||||
import type { FieldBasic } from '../../types';
|
||||
import { onFieldChange } from '../../actions/methodActions';
|
||||
import { Row } from './Row';
|
||||
|
||||
interface CoinSelectProps {
|
||||
field: Field<any>;
|
||||
field: FieldBasic<any>;
|
||||
onChange: typeof onFieldChange;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { ChangeEventHandler } from 'react';
|
||||
|
||||
import { Button } from '@trezor/components';
|
||||
|
||||
import { Row } from './Row';
|
||||
import type { Field } from '../../types';
|
||||
import type { FieldBasic } from '../../types';
|
||||
|
||||
interface FileProps {
|
||||
field: Field<any>;
|
||||
field: FieldBasic<any>;
|
||||
disabled?: boolean;
|
||||
onChange: (field: Field<any>, value: any) => any;
|
||||
onChange: (field: FieldBasic<any>, value: any) => any;
|
||||
}
|
||||
|
||||
const File = ({ disabled, field, onChange }: FileProps) => {
|
||||
const onFilesAdded = evt => {
|
||||
const onFilesAdded: ChangeEventHandler<HTMLInputElement> = evt => {
|
||||
if (disabled) return;
|
||||
const files = evt?.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Input as InputComponent } from '@trezor/components';
|
||||
|
||||
import { Field } from '../../types';
|
||||
import { FieldBasic } from '../../types';
|
||||
import { Row } from './Row';
|
||||
|
||||
interface InputProps {
|
||||
onChange: (field: Field<any>, value: string) => void;
|
||||
field: Field<any>;
|
||||
onChange: (field: FieldBasic<any>, value: string) => void;
|
||||
field: FieldBasic<any>;
|
||||
dataTest?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import styled from 'styled-components';
|
||||
|
||||
import { Textarea as TextareaComponent } from '@trezor/components';
|
||||
|
||||
import { Field } from '../../types';
|
||||
import { FieldBasic } from '../../types';
|
||||
|
||||
const Row = styled.div`
|
||||
display: block;
|
||||
@@ -10,8 +10,8 @@ const Row = styled.div`
|
||||
`;
|
||||
|
||||
interface TextareaProps {
|
||||
field: Field<string>;
|
||||
onChange: (field: Field<string>, value: string) => any;
|
||||
field: FieldBasic<string>;
|
||||
onChange: (field: FieldBasic<string>, value: string) => any;
|
||||
}
|
||||
|
||||
const Textarea = ({ field, onChange }: TextareaProps) => (
|
||||
|
||||
193
packages/connect-explorer-nextra/src/reducers/methodCommon.ts
Normal file
193
packages/connect-explorer-nextra/src/reducers/methodCommon.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import TrezorConnect from '@trezor/connect-web';
|
||||
import { setDeepValue } from '@trezor/schema-utils/src/utils';
|
||||
|
||||
import { Field, FieldBasic } from '../types';
|
||||
|
||||
export interface MethodState {
|
||||
name?: keyof typeof TrezorConnect;
|
||||
submitButton?: string;
|
||||
fields: Field<unknown>[];
|
||||
params: Record<string, unknown>;
|
||||
response?: unknown;
|
||||
javascriptCode?: string;
|
||||
addressValidation?: boolean;
|
||||
}
|
||||
|
||||
export const initialState: MethodState = {
|
||||
name: undefined,
|
||||
submitButton: undefined,
|
||||
fields: [],
|
||||
params: {},
|
||||
javascriptCode: undefined,
|
||||
response: undefined,
|
||||
addressValidation: false,
|
||||
};
|
||||
|
||||
// Converts the fields into a params object
|
||||
export const getParam = (field: FieldBasic<any>, $params: Record<string, any> = {}) => {
|
||||
const params = $params;
|
||||
if (field.omit) {
|
||||
return params;
|
||||
}
|
||||
if (field.optional && ((!field.value && field.value !== 0) || field.value === '')) {
|
||||
return params;
|
||||
}
|
||||
|
||||
let value: any;
|
||||
if ('defaultValue' in field) {
|
||||
if (field.defaultValue !== field.value) {
|
||||
value = field.value;
|
||||
}
|
||||
} else if (field.type === 'json') {
|
||||
try {
|
||||
if (typeof field.value === 'string') {
|
||||
if (field.value.length > 0) {
|
||||
value = JSON.parse(field.value);
|
||||
} else {
|
||||
value = undefined;
|
||||
}
|
||||
} else {
|
||||
value = field.value;
|
||||
}
|
||||
} catch (error) {
|
||||
value = `Invalid json, ${error.toString()}`;
|
||||
}
|
||||
} else if (field.type === 'function') {
|
||||
try {
|
||||
if (typeof field.value !== 'function') {
|
||||
throw new Error('Invalid function');
|
||||
}
|
||||
value = field.value;
|
||||
} catch (error) {
|
||||
value = `Invalid function, ${error.toString()}`;
|
||||
}
|
||||
} else if (field.type === 'number') {
|
||||
if (!Number.isNaN(Number.parseInt(field.value, 10))) {
|
||||
value = Number.parseInt(field.value, 10);
|
||||
}
|
||||
} else {
|
||||
value = field.value;
|
||||
}
|
||||
if (field.name) {
|
||||
setDeepValue(params, field.name.split('.'), value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
// Updates the javascript output based on the current params
|
||||
export const updateJavascript = (state: MethodState) => {
|
||||
const code =
|
||||
Object.keys(state.params).length > 0
|
||||
? JSON.stringify(
|
||||
state.params,
|
||||
(_, value) => {
|
||||
if (Object.prototype.toString.call(value) === '[object ArrayBuffer]') {
|
||||
return 'ArrayBuffer';
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
2,
|
||||
)
|
||||
: '';
|
||||
|
||||
return {
|
||||
...state,
|
||||
javascriptCode: `TrezorConnect.${state.name}(${code});`,
|
||||
};
|
||||
};
|
||||
|
||||
// Update params inner function, recursively called for nested fields
|
||||
export const updateParamsNested = (schema: Field<any>[]) => {
|
||||
let params: Record<string, any> = {};
|
||||
schema.forEach(field => {
|
||||
if (field.type === 'array') {
|
||||
const arr: Record<string, any>[] = [];
|
||||
field.items?.forEach(batch => {
|
||||
const batchParams = updateParamsNested(batch);
|
||||
arr.push(batchParams);
|
||||
});
|
||||
if (arr.length > 0 || !field.optional) {
|
||||
setDeepValue(params, field.name?.split('.'), arr);
|
||||
}
|
||||
} else {
|
||||
params = getParam(field, params);
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
// Update params in the state
|
||||
export const updateParams = (state: MethodState) => {
|
||||
const params: Record<string, any> = updateParamsNested(state.fields);
|
||||
|
||||
return updateJavascript({
|
||||
...state,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
// Set affected values
|
||||
export const setAffectedValues = (state: MethodState, field: Field<unknown>) => {
|
||||
if (!field.affect) return field;
|
||||
|
||||
const data = field.data?.find(d => d.value === field.value);
|
||||
if (data && data.affectedValue) {
|
||||
const affectedFieldNames = !Array.isArray(field.affect) ? [field.affect] : field.affect;
|
||||
const values = !Array.isArray(data.affectedValue)
|
||||
? [data.affectedValue]
|
||||
: data.affectedValue;
|
||||
|
||||
let root: Field<any>[] | undefined;
|
||||
if (typeof field.key === 'string') {
|
||||
const key = field.key.split('-');
|
||||
const bundle = state.fields.find(f => f.name === key[0]);
|
||||
if (bundle && bundle.type === 'array' && bundle.items) {
|
||||
root = bundle.items?.find((_batch, index) => index === Number.parseInt(key[1], 10));
|
||||
}
|
||||
} else {
|
||||
root = state.fields;
|
||||
}
|
||||
|
||||
affectedFieldNames.forEach((af, index) => {
|
||||
const affectedField = root?.find(f => f.name === af);
|
||||
if (affectedField && affectedField.type !== 'array') {
|
||||
affectedField.value = values[index];
|
||||
if (state.name === 'composeTransaction') {
|
||||
affectedField.value = values;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (field.affect && typeof field.affect === 'string' && field.value) {
|
||||
const affectedField = state.fields.find(f => f.name === field.affect);
|
||||
if (affectedField) {
|
||||
// @ts-expect-error todo: what is this?
|
||||
affectedField.value = field.value;
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
// Prepare bundle - set keys for nested fields
|
||||
export const prepareBundle = (field: Field<unknown>) => {
|
||||
if (field.type === 'array') {
|
||||
field.items.forEach((batch, index) => {
|
||||
batch.forEach(batchField => {
|
||||
batchField.key = `${field.name}-${index}`;
|
||||
if (field.key) {
|
||||
batchField.key = `${field.key}-${batchField.key}`;
|
||||
}
|
||||
if (batchField.type === 'array') {
|
||||
prepareBundle(batchField);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
138
packages/connect-explorer-nextra/src/reducers/methodInit.ts
Normal file
138
packages/connect-explorer-nextra/src/reducers/methodInit.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Optional, Kind, TSchema } from '@sinclair/typebox';
|
||||
|
||||
import type TrezorConnect from '@trezor/connect-web';
|
||||
|
||||
import {
|
||||
MethodState,
|
||||
initialState,
|
||||
prepareBundle,
|
||||
setAffectedValues,
|
||||
updateParams,
|
||||
} from './methodCommon';
|
||||
import { Field, FieldBasic } from '../types';
|
||||
|
||||
// Convert TypeBox schema to our fields
|
||||
const schemaToFields = (schema: TSchema): Field<any>[] => {
|
||||
if (schema[Kind] === 'Object') {
|
||||
return Object.keys(schema.properties).flatMap(key => {
|
||||
const field = schema.properties[key];
|
||||
/* if (field[Kind] === 'Object') {
|
||||
return [
|
||||
{
|
||||
name: key,
|
||||
label: key,
|
||||
type: 'json',
|
||||
value: undefined,
|
||||
},
|
||||
];
|
||||
} */
|
||||
|
||||
return schemaToFields(field).map(field => {
|
||||
const output = {
|
||||
...field,
|
||||
name: [key, field.name].filter(v => v).join('.'),
|
||||
optional: field.optional || schema[Optional] === 'Optional',
|
||||
};
|
||||
// TODO better handle optional parent
|
||||
if (output.type === 'array') {
|
||||
if (output.optional) {
|
||||
output.items = [];
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
});
|
||||
});
|
||||
} else if (schema[Kind] === 'Array') {
|
||||
const fields = schemaToFields(schema.items);
|
||||
|
||||
return [
|
||||
{
|
||||
name: '',
|
||||
type: 'array',
|
||||
batch: [
|
||||
{
|
||||
type: '',
|
||||
fields,
|
||||
},
|
||||
],
|
||||
items: schema[Optional] === 'Optional' ? [] : [fields],
|
||||
},
|
||||
];
|
||||
} else if (schema[Kind] === 'Intersect') {
|
||||
return schema.allOf?.flatMap(schemaToFields);
|
||||
} else if (schema[Kind] === 'Union') {
|
||||
const onlyLiterals = schema.anyOf?.every((s: TSchema) => s[Kind] === 'Literal');
|
||||
if (onlyLiterals) {
|
||||
const filtered = schema.anyOf?.filter(
|
||||
(s: TSchema, i: number) => s.const !== i.toString(),
|
||||
);
|
||||
const options = filtered.length > 0 ? filtered : schema.anyOf;
|
||||
|
||||
return [
|
||||
{
|
||||
name: '',
|
||||
type: 'select',
|
||||
value: schema.default,
|
||||
optional: schema[Optional] === 'Optional',
|
||||
data: options.map((s: TSchema) => ({ label: s.const, value: s.const })),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (schema.anyOf?.length > 0) {
|
||||
// TODO complex union handling - currently only first option is used
|
||||
return schemaToFields(schema.anyOf[0]).map(field => ({
|
||||
...field,
|
||||
optional: field.optional || schema[Optional] === 'Optional',
|
||||
value: field.type === 'array' ? undefined : field.value ?? schema.default,
|
||||
}));
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const typeMap: Record<string, FieldBasic<any>['type']> = {
|
||||
String: 'input',
|
||||
Number: 'number',
|
||||
Uint: 'number',
|
||||
Boolean: 'checkbox',
|
||||
} as const;
|
||||
|
||||
return [
|
||||
{
|
||||
name: '',
|
||||
type: typeMap[schema[Kind]] ?? 'input',
|
||||
value: schema.default,
|
||||
optional: schema[Optional] === 'Optional',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// Get method state
|
||||
export const getMethodState = (methodConfig?: Partial<MethodState>) => {
|
||||
if (!methodConfig) return initialState;
|
||||
|
||||
// clone object
|
||||
const state = {
|
||||
...JSON.parse(JSON.stringify(methodConfig)),
|
||||
// ...method,
|
||||
} as MethodState;
|
||||
|
||||
// set default values
|
||||
state.fields = state.fields.map(f => setAffectedValues(state, prepareBundle(f)));
|
||||
|
||||
console.log('state', state);
|
||||
|
||||
// set method params
|
||||
return updateParams(state);
|
||||
};
|
||||
|
||||
// Get method state from TypeBox schema
|
||||
export const getMethodStateFromSchema = (method: keyof typeof TrezorConnect, schema: TSchema) => {
|
||||
return getMethodState({
|
||||
name: method,
|
||||
fields: schemaToFields(schema),
|
||||
submitButton: 'Submit',
|
||||
});
|
||||
};
|
||||
@@ -5,192 +5,57 @@ import {
|
||||
REMOVE_BATCH,
|
||||
RESPONSE,
|
||||
SET_METHOD,
|
||||
SET_SCHEMA,
|
||||
} from '../actions/methodActions';
|
||||
import type { Action, Field, FieldWithBundle } from '../types';
|
||||
import type { Action, Field } from '../types';
|
||||
import {
|
||||
MethodState,
|
||||
initialState,
|
||||
prepareBundle,
|
||||
setAffectedValues,
|
||||
updateParams,
|
||||
} from './methodCommon';
|
||||
import { getMethodState, getMethodStateFromSchema } from './methodInit';
|
||||
|
||||
export interface MethodState {
|
||||
name?: string;
|
||||
submitButton: any;
|
||||
fields: (Field<any> | FieldWithBundle<any>)[];
|
||||
params: any;
|
||||
javascriptCode?: string;
|
||||
response?: any;
|
||||
addressValidation?: boolean;
|
||||
}
|
||||
|
||||
const initialState: MethodState = {
|
||||
name: undefined,
|
||||
submitButton: null,
|
||||
fields: [],
|
||||
params: {},
|
||||
javascriptCode: undefined,
|
||||
response: undefined,
|
||||
addressValidation: false,
|
||||
};
|
||||
|
||||
const getParam = (field: Field<any>, $params: Record<string, any> = {}) => {
|
||||
const params = $params;
|
||||
if (field.omit) {
|
||||
return params;
|
||||
}
|
||||
if (field.optional && ((!field.value && field.value !== 0) || field.value === '')) {
|
||||
return params;
|
||||
}
|
||||
if ('defaultValue' in field) {
|
||||
if (field.defaultValue !== field.value) {
|
||||
params[field.name] = field.value;
|
||||
}
|
||||
} else if (field.type === 'json') {
|
||||
try {
|
||||
if (typeof field.value === 'string') {
|
||||
if (field.value.length > 0) {
|
||||
params[field.name] = JSON.parse(field.value);
|
||||
} else {
|
||||
params[field.name] = undefined;
|
||||
}
|
||||
} else {
|
||||
params[field.name] = field.value;
|
||||
}
|
||||
} catch (error) {
|
||||
params[field.name] = `Invalid json, ${error.toString()}`;
|
||||
}
|
||||
} else if (field.type === 'function') {
|
||||
try {
|
||||
if (typeof field.value !== 'function') {
|
||||
throw new Error('Invalid function');
|
||||
}
|
||||
params[field.name] = field.value;
|
||||
} catch (error) {
|
||||
params[field.name] = `Invalid function, ${error.toString()}`;
|
||||
}
|
||||
} else if (field.type === 'number') {
|
||||
if (!Number.isNaN(Number.parseInt(field.value, 10))) {
|
||||
params[field.name] = Number.parseInt(field.value, 10);
|
||||
}
|
||||
} else {
|
||||
params[field.name] = field.value;
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
const updateJavascript = (state: MethodState) => {
|
||||
const code =
|
||||
Object.keys(state.params).length > 0
|
||||
? JSON.stringify(
|
||||
state.params,
|
||||
(_, value) => {
|
||||
if (Object.prototype.toString.call(value) === '[object ArrayBuffer]') {
|
||||
return 'ArrayBuffer';
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
2,
|
||||
)
|
||||
: '';
|
||||
|
||||
return {
|
||||
...state,
|
||||
javascriptCode: `TrezorConnect.${state.name}(${code});`,
|
||||
};
|
||||
};
|
||||
|
||||
const updateParams = (state: MethodState) => {
|
||||
const params: Record<string, any> = {};
|
||||
state.fields.forEach(field => {
|
||||
if (field.type === 'array') {
|
||||
const arr: Record<string, any>[] = [];
|
||||
field.items?.forEach(batch => {
|
||||
const batchParams = {};
|
||||
batch.forEach(batchField => {
|
||||
getParam(batchField, batchParams);
|
||||
});
|
||||
arr.push(batchParams);
|
||||
});
|
||||
params[field.name] = arr;
|
||||
} else {
|
||||
getParam(field, params);
|
||||
}
|
||||
});
|
||||
|
||||
return updateJavascript({
|
||||
...state,
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const setAffectedValues = (state: MethodState, field: any) => {
|
||||
if (!field.affect) return field;
|
||||
|
||||
const data = field.data?.find(d => d.value === field.value);
|
||||
if (data && data.affectedValue) {
|
||||
const affectedFieldNames = !Array.isArray(field.affect) ? [field.affect] : field.affect;
|
||||
const values = !Array.isArray(data.affectedValue)
|
||||
? [data.affectedValue]
|
||||
: data.affectedValue;
|
||||
|
||||
let root;
|
||||
if (typeof field.key === 'string') {
|
||||
const key = field.key.split('-');
|
||||
const bundle = state.fields.find(f => f.name === key[0]);
|
||||
if (bundle) {
|
||||
root = bundle.items?.find((_batch, index) => index === Number.parseInt(key[1], 10));
|
||||
}
|
||||
} else {
|
||||
root = state.fields;
|
||||
}
|
||||
|
||||
affectedFieldNames.forEach((af, index) => {
|
||||
const affectedField = root.find(f => f.name === af);
|
||||
if (affectedField) {
|
||||
affectedField.value = values[index];
|
||||
if (state.name === 'composeTransaction') {
|
||||
affectedField.value = values;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (field.affect && typeof field.affect === 'string' && field.value) {
|
||||
const affectedField = state.fields.find(f => f.name === field.affect);
|
||||
if (affectedField) {
|
||||
// @ts-expect-error todo: what is this?
|
||||
affectedField.value = field.value;
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
const prepareBundle = (field: any) => {
|
||||
if (field.type === 'array') {
|
||||
field.items!.forEach((batch, index) => {
|
||||
batch.forEach(batchField => {
|
||||
batchField.key = `${field.name}-${index}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
const findField = (state: MethodState, field: any) => {
|
||||
// Recursively find a field in the schema (inner function)
|
||||
const findFieldsNested = (
|
||||
schema: Field<any>[],
|
||||
field: Field<any>,
|
||||
currentDepth = 0,
|
||||
): Field<any> | undefined => {
|
||||
if (typeof field.key === 'string') {
|
||||
const key = field.key.split('-');
|
||||
const bundle = state.fields.find(f => f.name === key[0]);
|
||||
const batch = bundle?.items?.find((_batch, index) => index === Number.parseInt(key[1], 10));
|
||||
const bundle = schema.find(f => f.name === key[currentDepth * 2] && f.type === 'array');
|
||||
if (bundle?.type !== 'array') {
|
||||
return;
|
||||
}
|
||||
const batch = bundle.items.find(
|
||||
(_batch, index) => index === Number.parseInt(key[currentDepth * 2 + 1], 10),
|
||||
);
|
||||
if (!batch) return;
|
||||
if (key.length > currentDepth * 2 + 2) {
|
||||
return findFieldsNested(batch, field, currentDepth + 1);
|
||||
}
|
||||
|
||||
return batch.find(f => f.name === field.name);
|
||||
}
|
||||
|
||||
return state.fields.find(f => f.name === field.name);
|
||||
return schema.find(f => f.name === field.name);
|
||||
};
|
||||
|
||||
const onFieldChange = (state: MethodState, _field: any, value: any) => {
|
||||
// Find a field in the schema
|
||||
const findField = (state: MethodState, field: Field<any>) => {
|
||||
return findFieldsNested(state.fields, field);
|
||||
};
|
||||
|
||||
// Update field value
|
||||
const onFieldChange = (state: MethodState, _field: Field<any>, value: any) => {
|
||||
const newState = {
|
||||
...JSON.parse(JSON.stringify(state)),
|
||||
...state,
|
||||
};
|
||||
const field = findField(newState, _field);
|
||||
if (!field || field.type === 'array') return state;
|
||||
field.value = value;
|
||||
if (field.affect) {
|
||||
setAffectedValues(newState, field);
|
||||
@@ -199,45 +64,37 @@ const onFieldChange = (state: MethodState, _field: any, value: any) => {
|
||||
return updateParams(newState);
|
||||
};
|
||||
|
||||
const onFieldDataChange = (state: MethodState, _field: any, data: any) => {
|
||||
// Update field data
|
||||
const onFieldDataChange = (state: MethodState, _field: Field<any>, data: any) => {
|
||||
const newState = state;
|
||||
const field = findField(newState, _field);
|
||||
if (!field || field.type === 'array') return state;
|
||||
field.data = data;
|
||||
|
||||
return updateParams(newState);
|
||||
};
|
||||
|
||||
// initialization
|
||||
const getMethodState = (methodConfig?: any) => {
|
||||
if (!methodConfig) return initialState;
|
||||
// clone object
|
||||
const state = {
|
||||
...JSON.parse(JSON.stringify(methodConfig)),
|
||||
// ...method,
|
||||
};
|
||||
|
||||
// set default values
|
||||
state.fields = state.fields.map(f => setAffectedValues(state, prepareBundle(f)));
|
||||
|
||||
// set method params
|
||||
return updateParams(state);
|
||||
};
|
||||
|
||||
// Add new batch
|
||||
const onAddBatch = (state: MethodState, _field: Field<any>, item: any) => {
|
||||
const newState = JSON.parse(JSON.stringify(state));
|
||||
const field = newState.fields.find(f => f.name === _field.name);
|
||||
const field = findField(newState, _field);
|
||||
if (!field || field.type !== 'array') return state;
|
||||
field.items = [...field.items, item];
|
||||
prepareBundle(field);
|
||||
|
||||
return updateParams(newState);
|
||||
};
|
||||
|
||||
const onRemoveBatch = (state: MethodState, _field: any, _batch: any) => {
|
||||
const field = state.fields.find(f => f.name === _field.name);
|
||||
// Remove batch
|
||||
const onRemoveBatch = (state: MethodState, _field: Field<any>, _batch: any) => {
|
||||
const field = findField(state, _field);
|
||||
if (!field || field.type !== 'array') return state;
|
||||
const items = field?.items?.filter(batch => batch !== _batch);
|
||||
|
||||
const newState = JSON.parse(JSON.stringify(state));
|
||||
const newField = newState.fields.find(f => f.name === field?.name);
|
||||
const newField = findField(newState, field);
|
||||
if (!newField || newField.type !== 'array') return state;
|
||||
|
||||
newField.items = items;
|
||||
prepareBundle(newField);
|
||||
|
||||
@@ -249,6 +106,9 @@ export default function method(state: MethodState = initialState, action: Action
|
||||
case SET_METHOD:
|
||||
return getMethodState(action.methodConfig);
|
||||
|
||||
case SET_SCHEMA:
|
||||
return getMethodStateFromSchema(action.method, action.schema);
|
||||
|
||||
case FIELD_CHANGE:
|
||||
return onFieldChange(state, action.field, action.value);
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ const onOptionChange = <T>(state: ConnectState, field: Field<T>, value: T): Conn
|
||||
},
|
||||
};
|
||||
}
|
||||
// @ts-expect-error field name must be key of options
|
||||
newState.options[field.name] = value;
|
||||
|
||||
return newState;
|
||||
|
||||
@@ -23,15 +23,14 @@ export interface FieldData {
|
||||
affectedValue: string;
|
||||
}
|
||||
|
||||
export interface Field<Value> {
|
||||
export interface FieldCommon {
|
||||
key?: string;
|
||||
name: string;
|
||||
batch?: any[];
|
||||
items?: any[];
|
||||
data?: FieldData[];
|
||||
omit?: boolean;
|
||||
optional?: boolean;
|
||||
key: string;
|
||||
affect?: string;
|
||||
omit?: boolean;
|
||||
}
|
||||
|
||||
export interface FieldBasic<Value> extends FieldCommon {
|
||||
type:
|
||||
| 'input'
|
||||
| 'input-long'
|
||||
@@ -45,6 +44,8 @@ export interface Field<Value> {
|
||||
| 'file';
|
||||
value: Value;
|
||||
defaultValue?: Value;
|
||||
affect?: string;
|
||||
data?: FieldData[];
|
||||
}
|
||||
|
||||
interface Batch<Value> {
|
||||
@@ -52,9 +53,11 @@ interface Batch<Value> {
|
||||
fields: Field<Value>[];
|
||||
}
|
||||
|
||||
export interface FieldWithBundle<Value> {
|
||||
name: 'bundle';
|
||||
export interface FieldWithBundle<Value> extends FieldCommon {
|
||||
type: 'array';
|
||||
items: Field<Value>[][];
|
||||
batch: Batch<Value>[];
|
||||
items: Field<Value>[][];
|
||||
affect?: undefined;
|
||||
}
|
||||
|
||||
export type Field<Value> = FieldBasic<Value> | FieldWithBundle<Value>;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"plugins": [
|
||||
{ "name": "typescript-styled-plugin" }
|
||||
]
|
||||
@@ -21,6 +20,7 @@
|
||||
{ "path": "../connect-web" },
|
||||
{ "path": "../connect-webextension" },
|
||||
{ "path": "../protobuf" },
|
||||
{ "path": "../schema-utils" },
|
||||
{ "path": "../utils" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { JavaScriptTypeBuilder, TUnion, Hint, SchemaOptions, TLiteral } from '@sinclair/typebox';
|
||||
import {
|
||||
JavaScriptTypeBuilder,
|
||||
TUnion,
|
||||
Hint,
|
||||
SchemaOptions,
|
||||
TLiteral,
|
||||
TEnum,
|
||||
TEnumKey,
|
||||
TEnumValue,
|
||||
} from '@sinclair/typebox';
|
||||
|
||||
// UnionToIntersection<A | B> = A & B
|
||||
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => 0 : never) extends (
|
||||
@@ -38,4 +47,15 @@ export class KeyofEnumBuilder extends JavaScriptTypeBuilder {
|
||||
|
||||
return this.Union(keys, { ...options, [Hint]: 'KeyOfEnum' }) as TKeyOfEnum<T>;
|
||||
}
|
||||
|
||||
Enum<V extends TEnumValue, T extends Record<TEnumKey, V>>(
|
||||
schema: T,
|
||||
options?: SchemaOptions,
|
||||
): TEnum<T> {
|
||||
const anyOf = Object.entries(schema)
|
||||
.filter(([key, _value]) => typeof key === 'string' || !isNaN(key))
|
||||
.map(([key, value]) => this.Literal(value, { $id: key }));
|
||||
|
||||
return this.Union(anyOf, { ...options, [Hint]: 'Enum' }) as TEnum<T>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,23 @@ describe('enum', () => {
|
||||
const x: T2 = 'B';
|
||||
expect(x).toEqual('B');
|
||||
});
|
||||
|
||||
it('should work with normal enum', () => {
|
||||
enum E {
|
||||
A = 'a',
|
||||
B = 'b',
|
||||
}
|
||||
const schema = Type.Enum(E);
|
||||
type T = Static<typeof schema>;
|
||||
|
||||
const x: T = E.A;
|
||||
expect(x).toEqual('a');
|
||||
|
||||
// @ts-expect-error
|
||||
const y: T = 'C';
|
||||
expect(y).toEqual('C');
|
||||
|
||||
expect(Validate(schema, 'a')).toBe(true);
|
||||
expect(Validate(schema, 'b')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10259,6 +10259,7 @@ __metadata:
|
||||
"@trezor/connect-web": "workspace:^"
|
||||
"@trezor/connect-webextension": "workspace:^"
|
||||
"@trezor/protobuf": "workspace:^"
|
||||
"@trezor/schema-utils": "workspace:^"
|
||||
"@trezor/utils": "workspace:^"
|
||||
"@types/redux-logger": "npm:^3.0.11"
|
||||
babel-plugin-styled-components: "npm:^2.1.4"
|
||||
@@ -10735,7 +10736,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@trezor/schema-utils@workspace:*, @trezor/schema-utils@workspace:packages/schema-utils":
|
||||
"@trezor/schema-utils@workspace:*, @trezor/schema-utils@workspace:^, @trezor/schema-utils@workspace:packages/schema-utils":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@trezor/schema-utils@workspace:packages/schema-utils"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user