diff --git a/components/Select.tsx b/components/Select.tsx index 0b55e5b..9ad7d9c 100644 --- a/components/Select.tsx +++ b/components/Select.tsx @@ -3,13 +3,11 @@ import { mauve, mauveDark, purple, purpleDark } from '@radix-ui/colors' import { useTheme } from 'next-themes' import { styled } from '../stitches.config' import dynamic from 'next/dynamic' -import type { Props } from 'react-select' +import type { Props, StylesConfig } from 'react-select' const SelectInput = dynamic(() => import('react-select'), { ssr: false }) +const CreatableSelectInput = dynamic(() => import('react-select/creatable'), { ssr: false }) -// eslint-disable-next-line react/display-name -const Select = forwardRef((props, ref) => { - const { theme } = useTheme() - const isDark = theme === 'dark' +const getColors = (isDark: boolean) => { const colors: any = { // primary: pink.pink9, active: isDark ? purpleDark.purple9 : purple.purple9, @@ -30,95 +28,136 @@ const Select = forwardRef((props, ref) => { } colors.outline = colors.background colors.selected = colors.secondary + return colors +} + +const getStyles = (isDark: boolean) => { + const colors = getColors(isDark) + const styles: StylesConfig = { + container: provided => { + return { + ...provided, + position: 'relative', + width: '100%' + } + }, + singleValue: provided => ({ + ...provided, + color: colors.mauve12 + }), + menu: provided => ({ + ...provided, + backgroundColor: colors.dropDownBg + }), + control: (provided, state) => { + return { + ...provided, + minHeight: 0, + border: '0px', + backgroundColor: colors.mauve4, + boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}` + } + }, + input: provided => { + return { + ...provided, + color: '$text' + } + }, + multiValue: provided => { + return { + ...provided, + backgroundColor: colors.mauve8 + } + }, + multiValueLabel: provided => { + return { + ...provided, + color: colors.mauve12 + } + }, + multiValueRemove: provided => { + return { + ...provided, + ':hover': { + background: colors.mauve9 + } + } + }, + option: (provided, state) => { + return { + ...provided, + color: colors.searchText, + backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg, + ':hover': { + backgroundColor: colors.active, + color: '#ffffff' + }, + ':selected': { + backgroundColor: 'red' + } + } + }, + indicatorSeparator: provided => { + return { + ...provided, + backgroundColor: colors.secondary + } + }, + dropdownIndicator: (provided, state) => { + return { + ...provided, + padding: 6, + color: state.isFocused ? colors.border : colors.secondary, + ':hover': { + color: colors.border + } + } + }, + clearIndicator: provided => { + return { + ...provided, + padding: 6, + color: colors.secondary, + ':hover': { + color: colors.border + } + } + } + } + return styles +} + +// eslint-disable-next-line react/display-name +const Select = forwardRef((props, ref) => { + const { theme } = useTheme() + const isDark = theme === 'dark' + const styles = getStyles(isDark) return ( { - return { - ...provided, - position: 'relative', - width: '100%', - } - }, - singleValue: provided => ({ - ...provided, - color: colors.mauve12 - }), - menu: provided => ({ - ...provided, - backgroundColor: colors.dropDownBg - }), - control: (provided, state) => { - return { - ...provided, - minHeight: 0, - border: '0px', - backgroundColor: colors.mauve4, - boxShadow: `0 0 0 1px ${state.isFocused ? colors.border : colors.secondary}` - } - }, - input: provided => { - return { - ...provided, - color: '$text' - } - }, - multiValue: provided => { - return { - ...provided, - backgroundColor: colors.mauve8 - } - }, - multiValueLabel: provided => { - return { - ...provided, - color: colors.mauve12 - } - }, - multiValueRemove: provided => { - return { - ...provided, - ':hover': { - background: colors.mauve9 - } - } - }, - option: (provided, state) => { - return { - ...provided, - color: colors.searchText, - backgroundColor: state.isFocused ? colors.activeLight : colors.dropDownBg, - ':hover': { - backgroundColor: colors.active, - color: '#ffffff' - }, - ':selected': { - backgroundColor: 'red' - } - } - }, - indicatorSeparator: provided => { - return { - ...provided, - backgroundColor: colors.secondary - } - }, - dropdownIndicator: (provided, state) => { - return { - ...provided, - padding: 6, - color: state.isFocused ? colors.border : colors.secondary, - ':hover': { - color: colors.border, - } - } - } - }} + styles={styles} + {...props} + /> + ) +}) + +// eslint-disable-next-line react/display-name +const Creatable = forwardRef((props, ref) => { + const { theme } = useTheme() + const isDark = theme === 'dark' + const styles = getStyles(isDark) + return ( + `Enter "${label}"`} + menuPosition={props.menuPosition || 'fixed'} + styles={styles} {...props} /> ) }) export default styled(Select, {}) +export const CreatableSelect = styled(Creatable, {}) diff --git a/components/Transaction/index.tsx b/components/Transaction/index.tsx index da10c1e..1d5038e 100644 --- a/components/Transaction/index.tsx +++ b/components/Transaction/index.tsx @@ -43,7 +43,6 @@ const Transaction: FC = ({ header, state: txState, ...props }) (state: Partial = txState) => { const { selectedTransaction, - selectedDestAccount, selectedAccount, txFields, selectedFlags, @@ -52,7 +51,6 @@ const Transaction: FC = ({ header, state: txState, ...props }) } = state const TransactionType = selectedTransaction?.value || null - const Destination = selectedDestAccount?.value || txFields?.Destination const Account = selectedAccount?.value || null const Flags = combineFlags(selectedFlags?.map(flag => flag.value)) || txFields?.Flags const HookParameters = Object.entries(hookParameters || {}).reduce< @@ -75,7 +73,6 @@ const Transaction: FC = ({ header, state: txState, ...props }) HookParameters, Flags, TransactionType, - Destination, Account, Memos }) @@ -128,11 +125,12 @@ const Transaction: FC = ({ header, state: txState, ...props }) throw Error('Account must be selected from imported accounts!') } const options = prepareOptions(st) - - const fields = getTxFields(options.TransactionType) - if (fields.Destination && !options.Destination) { - throw Error('Destination account is required!') - } + // delete unnecessary fields + Object.keys(options).forEach(field => { + if (!options[field]) { + delete options[field] + } + }) await sendTransaction(account, options, { logPrefix }) } catch (error) { @@ -167,13 +165,6 @@ const Transaction: FC = ({ header, state: txState, ...props }) selectedTransaction: transactionType } - if (fields.Destination !== undefined) { - nwState.selectedDestAccount = null - fields.Destination = '' - } else { - fields.Destination = undefined - } - if (transactionType?.value && transactionFlags[transactionType?.value] && fields.Flags) { nwState.selectedFlags = extractFlags(transactionType.value, fields.Flags) fields.Flags = undefined diff --git a/components/Transaction/ui.tsx b/components/Transaction/ui.tsx index eec1d62..b69fccd 100644 --- a/components/Transaction/ui.tsx +++ b/components/Transaction/ui.tsx @@ -1,15 +1,14 @@ -import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { FC, ReactNode, useCallback, useEffect, useState } from 'react' import Container from '../Container' import Flex from '../Flex' import Input from '../Input' -import Select from '../Select' +import Select, { CreatableSelect } from '../Select' import Text from '../Text' import { SelectOption, TransactionState, transactionsOptions, TxFields, - getTxFields, defaultTransactionType } from '../../state/transactions' import { useSnapshot } from 'valtio' @@ -20,7 +19,7 @@ import Textarea from '../Textarea' import { getFlags } from '../../state/constants/flags' import { Plus, Trash } from 'phosphor-react' import AccountSequence from '../Sequence' -import { typeIs } from '../../utils/helpers' +import { capitalize, typeIs } from '../../utils/helpers' interface UIProps { setState: (pTx?: Partial | undefined) => TransactionState | undefined @@ -38,28 +37,14 @@ export const TxUI: FC = ({ switchToJson }) => { const { accounts } = useSnapshot(state) - const { - selectedAccount, - selectedDestAccount, - selectedTransaction, - txFields, - selectedFlags, - hookParameters, - memos - } = txState + const { selectedAccount, selectedTransaction, txFields, selectedFlags, hookParameters, memos } = + txState const accountOptions: SelectOption[] = accounts.map(acc => ({ label: acc.name, value: acc.address })) - const destAccountOptions: SelectOption[] = accounts - .map(acc => ({ - label: acc.name, - value: acc.address - })) - .filter(acc => acc.value !== selectedAccount?.value) - const flagsOptions: SelectOption[] = Object.entries( getFlags(selectedTransaction?.value) || {} ).map(([label, value]) => ({ @@ -136,15 +121,7 @@ export const TxUI: FC = ({ } }, [handleChangeTxType, selectedTransaction?.value]) - const fields = useMemo( - () => getTxFields(selectedTransaction?.value), - [selectedTransaction?.value] - ) - const richFields = ['TransactionType', 'Account', 'HookParameters', 'Memos'] - if (fields.Destination !== undefined) { - richFields.push('Destination') - } if (flagsOptions.length) { richFields.push('Flags') @@ -192,18 +169,6 @@ export const TxUI: FC = ({ - {richFields.includes('Destination') && ( - - = ({ value = _value?.toString() } + const isAccount = typeIs(_value, 'object') && _value.$type === 'account' const isXrpAmount = typeIs(_value, 'object') && _value.$type === 'amount.xrp' const isTokenAmount = typeIs(_value, 'object') && _value.$type === 'amount.token' const isJson = typeof _value === 'object' && _value.$type === 'json' @@ -255,8 +221,14 @@ export const TxUI: FC = ({ {isTokenAmount ? ( - - + {/* = ({ issuer: e.target.value }) } - /> + /> */} = ({ }} /> = ({ }) }} /> + + { + setRawField(field, 'amount.token', { + ...tokenAmount, + issuer: value + }) + }} + /> + ) : ( = ({ ) } + if (isAccount) { + return ( + + + + ) + } return ( {isJson ? ( @@ -535,6 +527,35 @@ export const TxUI: FC = ({ ) } +export const CreatableAccount: FC<{ + value: string | undefined + field: keyof TxFields + placeholder?: string + setField: (field: keyof TxFields, value: string, opFields?: TxFields) => void +}> = ({ value, field, setField, placeholder }) => { + const { accounts } = useSnapshot(state) + const accountOptions: SelectOption[] = accounts.map(acc => ({ + label: acc.name, + value: acc.address + })) + const label = accountOptions.find(a => a.value === value)?.label || value + const val = { + value, + label + } + placeholder = placeholder || `${capitalize(field)} account` + return ( + setField(field, acc?.value)} + /> + ) +} + export const TxField: FC<{ label: string; children: ReactNode; multiLine?: boolean }> = ({ label, children, diff --git a/content/transactions.json b/content/transactions.json index 930ea34..355ac09 100644 --- a/content/transactions.json +++ b/content/transactions.json @@ -2,7 +2,10 @@ { "TransactionType": "AccountDelete", "Account": "rWYkbWkCeg8dP6rXALnjgZSjjLyih5NXm", - "Destination": "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe", + "Destination": { + "$type": "account", + "$value": "" + }, "DestinationTag": 13, "Fee": "2000000", "Sequence": 2470665, @@ -40,7 +43,10 @@ { "TransactionType": "CheckCreate", "Account": "rUn84CUYbNjRoTQ6mSW7BVJPSVJNLb1QLo", - "Destination": "rfkE1aSy9G8Upk4JssnwBxhEv5p4mn2KTy", + "Destination": { + "$type": "account", + "$value": "" + }, "SendMax": "100000000", "Expiration": 570113521, "InvoiceID": "6F1DFD1D0FE8A32E40E1F2C05CF1C15545BAB56B617F9C6C2D63A6B704BEF59B", @@ -58,7 +64,10 @@ { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "TransactionType": "EscrowCancel", - "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Owner": { + "$type": "account", + "$value": "" + }, "OfferSequence": 7, "Fee": "10" }, @@ -69,7 +78,10 @@ "$value": "100", "$type": "amount.xrp" }, - "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + "Destination": { + "$type": "account", + "$value": "" + }, "CancelAfter": 533257958, "FinishAfter": 533171558, "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", @@ -80,7 +92,10 @@ { "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", "TransactionType": "EscrowFinish", - "Owner": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", + "Owner": { + "$type": "account", + "$value": "" + }, "OfferSequence": 7, "Condition": "A0258020E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855810100", "Fulfillment": "A0028000", @@ -127,7 +142,14 @@ "$type": "amount.xrp" }, "Flags": "1", - "Destination": "", + "Destination": { + "$type": "account", + "$value": "" + }, + "Owner": { + "$type": "account", + "$value": "" + }, "Fee": "10" }, { @@ -162,7 +184,10 @@ { "TransactionType": "Payment", "Account": "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn", - "Destination": "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", + "Destination": { + "$type": "account", + "$value": "" + }, "Amount": { "$value": "100", "$type": "amount.xrp" @@ -178,7 +203,10 @@ "$value": "100", "$type": "amount.xrp" }, - "Destination": "rsA2LpzuawewSBQXkiju3YQTMzW13pAAdW", + "Destination": { + "$type": "account", + "$value": "" + }, "SettleDelay": 86400, "PublicKey": "32D2471DB72B27E3310F355BB33E339BF26F8392D5A93D3BC0FC3B566612DA0F0A", "CancelAfter": 533171558, diff --git a/state/actions/deleteAccount.ts b/state/actions/deleteAccount.ts index b806d8a..5c41b4a 100644 --- a/state/actions/deleteAccount.ts +++ b/state/actions/deleteAccount.ts @@ -14,11 +14,4 @@ export const deleteAccount = (addr?: string) => { if (!acc) return acc.label = acc.value }) - transactionsState.transactions - .filter(t => t.state.selectedDestAccount?.value === addr) - .forEach(t => { - const acc = t.state.selectedDestAccount - if (!acc) return - acc.label = acc.value - }) } diff --git a/state/actions/sendTransaction.tsx b/state/actions/sendTransaction.tsx index 64a426c..f58741b 100644 --- a/state/actions/sendTransaction.tsx +++ b/state/actions/sendTransaction.tsx @@ -10,7 +10,6 @@ interface TransactionOptions { TransactionType: string Account?: string Fee?: string - Destination?: string [index: string]: any } interface OtherOptions { diff --git a/state/transactions.ts b/state/transactions.ts index 411e6c3..687e7b9 100644 --- a/state/transactions.ts +++ b/state/transactions.ts @@ -28,7 +28,6 @@ export type Memos = { export interface TransactionState { selectedTransaction: SelectOption | null selectedAccount: SelectOption | null - selectedDestAccount: SelectOption | null selectedFlags: SelectOption[] | null hookParameters: HookParameters memos: Memos @@ -51,7 +50,6 @@ export type TxFields = Omit< export const defaultTransaction: TransactionState = { selectedTransaction: null, selectedAccount: null, - selectedDestAccount: null, selectedFlags: null, hookParameters: {}, memos: {}, @@ -131,7 +129,6 @@ export const modifyTxState = ( return tx.state } -// state to tx options export const prepareTransaction = (data: any) => { let options = { ...data } @@ -143,10 +140,9 @@ export const prepareTransaction = (data: any) => { if (_value.$value) { options[field] = (+(_value as any).$value * 1000000 + '') } else { - options[field] = undefined + options[field] = "" } } - // amount.token if (_value.$type === 'amount.token') { if (typeIs(_value.$value, 'string')) { @@ -157,7 +153,10 @@ export const prepareTransaction = (data: any) => { options[field] = undefined } } - + // account + if (_value.$type === 'account') { + options[field] = (_value.$value as any)?.toString() || "" + } // json if (_value.$type === 'json') { const val = _value.$value; @@ -172,17 +171,9 @@ export const prepareTransaction = (data: any) => { } }) - // delete unnecessary fields - Object.keys(options).forEach(field => { - if (!options[field]) { - delete options[field] - } - }) - return options } -// editor value to state export const prepareState = (value: string, transactionType?: string) => { const options = parseJSON(value) if (!options) { @@ -192,7 +183,7 @@ export const prepareState = (value: string, transactionType?: string) => { return } - const { Account, TransactionType, Destination, HookParameters, Memos, ...rest } = options + const { Account, TransactionType, HookParameters, Memos, ...rest } = options let tx: Partial = {} const schema = getTxFields(transactionType) @@ -238,24 +229,6 @@ export const prepareState = (value: string, transactionType?: string) => { }, {}) } - if (schema.Destination !== undefined) { - const dest = state.accounts.find(acc => acc.address === Destination) - if (dest) { - tx.selectedDestAccount = { - label: dest.name, - value: dest.address - } - } else if (Destination) { - tx.selectedDestAccount = { - label: Destination, - value: Destination - } - } else { - tx.selectedDestAccount = null - } - } else if (Destination) { - rest.Destination = Destination - } if (getFlags(TransactionType) && rest.Flags) { const flags = extractFlags(TransactionType, rest.Flags) @@ -271,19 +244,27 @@ export const prepareState = (value: string, transactionType?: string) => { const isAmount = schemaVal && typeIs(schemaVal, "object") && schemaVal.$type.startsWith('amount.'); + const isAccount = schemaVal && + typeIs(schemaVal, "object") && + schemaVal.$type.startsWith("account"); if (isAmount && ["number", "string"].includes(typeof value)) { rest[field] = { - $type: 'amount.xrp', // Maybe have $type map or something + $type: 'amount.xrp', // TODO narrow typed $type. $value: +value / 1000000 // ! maybe use bigint? } - } - else if (isAmount && typeof value === 'object') { + } else if (isAmount && typeof value === 'object') { rest[field] = { $type: 'amount.token', $value: value } - } else if (typeof value === 'object') { + } else if (isAccount) { + rest[field] = { + $type: "account", + $value: value?.toString() || "" + } + } + else if (typeof value === 'object') { rest[field] = { $type: 'json', $value: value