Merge pull request #162 from eqlabs/feat/transaction-persistence
Persisted transactions and debug stream state.
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "./Dialog";
|
} from "./Dialog";
|
||||||
import { css } from "../stitches.config";
|
import { css } from "../stitches.config";
|
||||||
import { Input } from "./Input";
|
import { Input, Label } from "./Input";
|
||||||
import truncate from "../utils/truncate";
|
import truncate from "../utils/truncate";
|
||||||
|
|
||||||
const labelStyle = css({
|
const labelStyle = css({
|
||||||
@@ -491,7 +491,7 @@ const ImportAccountDialog = () => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Import account</DialogTitle>
|
<DialogTitle>Import account</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<label>Add account secret</label>
|
<Label>Add account secret</Label>
|
||||||
<Input
|
<Input
|
||||||
name="secret"
|
name="secret"
|
||||||
type="password"
|
type="password"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { proxy, ref, useSnapshot } from "valtio";
|
import { proxy, ref, useSnapshot } from "valtio";
|
||||||
import { Select } from ".";
|
import { Select } from ".";
|
||||||
import state, { ILog } from "../state";
|
import state, { ILog, transactionsState } from "../state";
|
||||||
import { extractJSON } from "../utils/json";
|
import { extractJSON } from "../utils/json";
|
||||||
import LogBox from "./LogBox";
|
import LogBox from "./LogBox";
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ interface ISelect<T = string> {
|
|||||||
value: T;
|
value: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamState = proxy({
|
export const streamState = proxy({
|
||||||
selectedAccount: null as ISelect | null,
|
selectedAccount: null as ISelect | null,
|
||||||
logs: [] as ILog[],
|
logs: [] as ILog[],
|
||||||
socket: undefined as WebSocket | undefined,
|
socket: undefined as WebSocket | undefined,
|
||||||
@@ -18,9 +18,10 @@ const streamState = proxy({
|
|||||||
|
|
||||||
const DebugStream = () => {
|
const DebugStream = () => {
|
||||||
const { selectedAccount, logs, socket } = useSnapshot(streamState);
|
const { selectedAccount, logs, socket } = useSnapshot(streamState);
|
||||||
|
const { activeHeader: activeTxTab } = useSnapshot(transactionsState);
|
||||||
const { accounts } = useSnapshot(state);
|
const { accounts } = useSnapshot(state);
|
||||||
|
|
||||||
const accountOptions = accounts.map((acc) => ({
|
const accountOptions = accounts.map(acc => ({
|
||||||
label: acc.name,
|
label: acc.name,
|
||||||
value: acc.address,
|
value: acc.address,
|
||||||
}));
|
}));
|
||||||
@@ -33,7 +34,7 @@ const DebugStream = () => {
|
|||||||
options={accountOptions}
|
options={accountOptions}
|
||||||
hideSelectedOptions
|
hideSelectedOptions
|
||||||
value={selectedAccount}
|
value={selectedAccount}
|
||||||
onChange={(acc) => (streamState.selectedAccount = acc as any)}
|
onChange={acc => (streamState.selectedAccount = acc as any)}
|
||||||
css={{ width: "100%" }}
|
css={{ width: "100%" }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -133,6 +134,16 @@ const DebugStream = () => {
|
|||||||
socket.removeEventListener("error", onError);
|
socket.removeEventListener("error", onError);
|
||||||
};
|
};
|
||||||
}, [prepareLog, selectedAccount?.value, socket]);
|
}, [prepareLog, selectedAccount?.value, socket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const account = transactionsState.transactions.find(
|
||||||
|
tx => tx.header === activeTxTab
|
||||||
|
)?.state.selectedAccount;
|
||||||
|
|
||||||
|
if (account && account.value !== streamState.selectedAccount?.value)
|
||||||
|
streamState.selectedAccount = account;
|
||||||
|
}, [activeTxTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LogBox
|
<LogBox
|
||||||
enhanced
|
enhanced
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
} from "./Dialog";
|
} from "./Dialog";
|
||||||
import Flex from "./Flex";
|
import Flex from "./Flex";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import Input from "./Input";
|
import { Input, Label } from "./Input";
|
||||||
import Text from "./Text";
|
import Text from "./Text";
|
||||||
import Tooltip from "./Tooltip";
|
import Tooltip from "./Tooltip";
|
||||||
import {
|
import {
|
||||||
@@ -222,7 +222,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Create new file</DialogTitle>
|
<DialogTitle>Create new file</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<label>Filename</label>
|
<Label>Filename</Label>
|
||||||
<Input
|
<Input
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={(e) => setFilename(e.target.value)}
|
onChange={(e) => setFilename(e.target.value)}
|
||||||
@@ -524,7 +524,7 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Editor settings</DialogTitle>
|
<DialogTitle>Editor settings</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<label>Tab size</label>
|
<Label>Tab size</Label>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { styled } from "../stitches.config";
|
import { styled } from "../stitches.config";
|
||||||
|
import * as LabelPrim from '@radix-ui/react-label';
|
||||||
|
|
||||||
export const Input = styled("input", {
|
export const Input = styled("input", {
|
||||||
// Reset
|
// Reset
|
||||||
@@ -158,3 +159,11 @@ const ReffedInput = React.forwardRef<
|
|||||||
>((props, ref) => <Input {...props} ref={ref} />);
|
>((props, ref) => <Input {...props} ref={ref} />);
|
||||||
|
|
||||||
export default ReffedInput;
|
export default ReffedInput;
|
||||||
|
|
||||||
|
|
||||||
|
const LabelRoot = (props: LabelPrim.LabelProps) => <LabelPrim.Root {...props} />
|
||||||
|
|
||||||
|
export const Label = styled(LabelRoot, {
|
||||||
|
display: 'inline-block',
|
||||||
|
mb: '$1'
|
||||||
|
})
|
||||||
@@ -52,6 +52,7 @@ const Select = forwardRef<any, Props>((props, ref) => {
|
|||||||
control: (provided, state) => {
|
control: (provided, state) => {
|
||||||
return {
|
return {
|
||||||
...provided,
|
...provided,
|
||||||
|
minHeight: 0,
|
||||||
border: "0px",
|
border: "0px",
|
||||||
backgroundColor: colors.mauve4,
|
backgroundColor: colors.mauve4,
|
||||||
boxShadow: `0 0 0 1px ${
|
boxShadow: `0 0 0 1px ${
|
||||||
@@ -118,32 +119,6 @@ const Select = forwardRef<any, Props>((props, ref) => {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
// theme={(theme) => ({
|
|
||||||
// ...theme,
|
|
||||||
// spacing: {
|
|
||||||
// ...theme.spacing,
|
|
||||||
// controlHeight: 30,
|
|
||||||
// },
|
|
||||||
// colors: {
|
|
||||||
// primary: colors.selected,
|
|
||||||
// primary25: colors.active,
|
|
||||||
// primary50: colors.primary,
|
|
||||||
// primary75: colors.primary,
|
|
||||||
// danger: colors.primary,
|
|
||||||
// dangerLight: colors.primary,
|
|
||||||
// neutral0: colors.background,
|
|
||||||
// neutral5: colors.primary,
|
|
||||||
// neutral10: colors.primary,
|
|
||||||
// neutral20: colors.outline,
|
|
||||||
// neutral30: colors.primary,
|
|
||||||
// neutral40: colors.primary,
|
|
||||||
// neutral50: colors.placeholder,
|
|
||||||
// neutral60: colors.primary,
|
|
||||||
// neutral70: colors.primary,
|
|
||||||
// neutral80: colors.searchText,
|
|
||||||
// neutral90: colors.primary,
|
|
||||||
// },
|
|
||||||
// })}
|
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
DialogClose,
|
DialogClose,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "./Dialog";
|
} from "./Dialog";
|
||||||
import { Input } from "./Input";
|
import { Input, Label } from "./Input";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
SubmitHandler,
|
SubmitHandler,
|
||||||
@@ -132,7 +132,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
|
|||||||
<DialogDescription as="div">
|
<DialogDescription as="div">
|
||||||
<Stack css={{ width: "100%", flex: 1 }}>
|
<Stack css={{ width: "100%", flex: 1 }}>
|
||||||
<Box css={{ width: "100%" }}>
|
<Box css={{ width: "100%" }}>
|
||||||
<label>Invoke on transactions</label>
|
<Label>Invoke on transactions</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="Invoke"
|
name="Invoke"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -151,7 +151,7 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ width: "100%" }}>
|
<Box css={{ width: "100%" }}>
|
||||||
<label>Hook Namespace Seed</label>
|
<Label>Hook Namespace Seed</Label>
|
||||||
<Input
|
<Input
|
||||||
{...register("HookNamespace", { required: true })}
|
{...register("HookNamespace", { required: true })}
|
||||||
autoComplete={"off"}
|
autoComplete={"off"}
|
||||||
@@ -165,14 +165,14 @@ export const SetHookDialog: React.FC<{ account: IAccount }> = ({ account }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box css={{ mt: "$3" }}>
|
<Box css={{ mt: "$3" }}>
|
||||||
<label>Hook Namespace (sha256)</label>
|
<Label>Hook Namespace (sha256)</Label>
|
||||||
<Input readOnly value={hashedNamespace} />
|
<Input readOnly value={hashedNamespace} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ width: "100%" }}>
|
<Box css={{ width: "100%" }}>
|
||||||
<label style={{ marginBottom: "10px", display: "block" }}>
|
<Label style={{ marginBottom: "10px", display: "block" }}>
|
||||||
Hook parameters
|
Hook parameters
|
||||||
</label>
|
</Label>
|
||||||
<Stack>
|
<Stack>
|
||||||
{fields.map((field, index) => (
|
{fields.map((field, index) => (
|
||||||
<Stack key={field.id}>
|
<Stack key={field.id}>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
} from "react";
|
} from "react";
|
||||||
import type { ReactNode, ReactElement } from "react";
|
import type { ReactNode, ReactElement } from "react";
|
||||||
import { Box, Button, Flex, Input, Stack, Text } from ".";
|
import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@@ -29,7 +29,7 @@ interface TabProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO customise strings shown
|
// TODO customise messages shown
|
||||||
interface Props {
|
interface Props {
|
||||||
activeIndex?: number;
|
activeIndex?: number;
|
||||||
activeHeader?: string;
|
activeHeader?: string;
|
||||||
@@ -40,6 +40,7 @@ interface Props {
|
|||||||
forceDefaultExtension?: boolean;
|
forceDefaultExtension?: boolean;
|
||||||
onCreateNewTab?: (name: string) => any;
|
onCreateNewTab?: (name: string) => any;
|
||||||
onCloseTab?: (index: number, header?: string) => any;
|
onCloseTab?: (index: number, header?: string) => any;
|
||||||
|
onChangeActive?: (index: number, header?: string) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tab = (props: TabProps) => null;
|
export const Tab = (props: TabProps) => null;
|
||||||
@@ -52,11 +53,12 @@ export const Tabs = ({
|
|||||||
keepAllAlive = false,
|
keepAllAlive = false,
|
||||||
onCreateNewTab,
|
onCreateNewTab,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
|
onChangeActive,
|
||||||
defaultExtension = "",
|
defaultExtension = "",
|
||||||
forceDefaultExtension,
|
forceDefaultExtension,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [active, setActive] = useState(activeIndex || 0);
|
const [active, setActive] = useState(activeIndex || 0);
|
||||||
const tabs: TabProps[] = children.map((elem) => elem.props);
|
const tabs: TabProps[] = children.map(elem => elem.props);
|
||||||
|
|
||||||
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
||||||
const [tabname, setTabname] = useState("");
|
const [tabname, setTabname] = useState("");
|
||||||
@@ -68,8 +70,9 @@ export const Tabs = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeHeader) {
|
if (activeHeader) {
|
||||||
const idx = tabs.findIndex((tab) => tab.header === activeHeader);
|
const idx = tabs.findIndex(tab => tab.header === activeHeader);
|
||||||
setActive(idx);
|
if (idx !== -1) setActive(idx);
|
||||||
|
else setActive(0);
|
||||||
}
|
}
|
||||||
}, [activeHeader, tabs]);
|
}, [activeHeader, tabs]);
|
||||||
|
|
||||||
@@ -80,7 +83,7 @@ export const Tabs = ({
|
|||||||
|
|
||||||
const validateTabname = useCallback(
|
const validateTabname = useCallback(
|
||||||
(tabname: string): { error: string | null } => {
|
(tabname: string): { error: string | null } => {
|
||||||
if (tabs.find((tab) => tab.header === tabname)) {
|
if (tabs.find(tab => tab.header === tabname)) {
|
||||||
return { error: "Name already exists." };
|
return { error: "Name already exists." };
|
||||||
}
|
}
|
||||||
return { error: null };
|
return { error: null };
|
||||||
@@ -88,6 +91,14 @@ export const Tabs = ({
|
|||||||
[tabs]
|
[tabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleActiveChange = useCallback(
|
||||||
|
(idx: number, header?: string) => {
|
||||||
|
setActive(idx);
|
||||||
|
onChangeActive?.(idx, header);
|
||||||
|
},
|
||||||
|
[onChangeActive]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreateTab = useCallback(() => {
|
const handleCreateTab = useCallback(() => {
|
||||||
// add default extension in case omitted
|
// add default extension in case omitted
|
||||||
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
|
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
|
||||||
@@ -103,11 +114,20 @@ export const Tabs = ({
|
|||||||
|
|
||||||
setIsNewtabDialogOpen(false);
|
setIsNewtabDialogOpen(false);
|
||||||
setTabname("");
|
setTabname("");
|
||||||
// switch to new tab?
|
|
||||||
setActive(tabs.length);
|
|
||||||
|
|
||||||
onCreateNewTab?.(_tabname);
|
onCreateNewTab?.(_tabname);
|
||||||
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
|
|
||||||
|
// switch to new tab?
|
||||||
|
handleActiveChange(tabs.length, _tabname);
|
||||||
|
}, [
|
||||||
|
tabname,
|
||||||
|
defaultExtension,
|
||||||
|
forceDefaultExtension,
|
||||||
|
validateTabname,
|
||||||
|
onCreateNewTab,
|
||||||
|
handleActiveChange,
|
||||||
|
tabs.length,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleCloseTab = useCallback(
|
const handleCloseTab = useCallback(
|
||||||
(idx: number) => {
|
(idx: number) => {
|
||||||
@@ -128,7 +148,7 @@ export const Tabs = ({
|
|||||||
gap: "$3",
|
gap: "$3",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexWrap: "nowrap",
|
flexWrap: "nowrap",
|
||||||
marginBottom: "-1px",
|
marginBottom: "$2",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
}}
|
}}
|
||||||
@@ -138,8 +158,8 @@ export const Tabs = ({
|
|||||||
key={tab.header}
|
key={tab.header}
|
||||||
role="tab"
|
role="tab"
|
||||||
tabIndex={idx}
|
tabIndex={idx}
|
||||||
onClick={() => setActive(idx)}
|
onClick={() => handleActiveChange(idx, tab.header)}
|
||||||
onKeyPress={() => setActive(idx)}
|
onKeyPress={() => handleActiveChange(idx, tab.header)}
|
||||||
outline={active !== idx}
|
outline={active !== idx}
|
||||||
size="sm"
|
size="sm"
|
||||||
css={{
|
css={{
|
||||||
@@ -192,11 +212,11 @@ export const Tabs = ({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>Create new tab</DialogTitle>
|
<DialogTitle>Create new tab</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<label>Tabname</label>
|
<Label>Tabname</Label>
|
||||||
<Input
|
<Input
|
||||||
value={tabname}
|
value={tabname}
|
||||||
onChange={(e) => setTabname(e.target.value)}
|
onChange={e => setTabname(e.target.value)}
|
||||||
onKeyPress={(e) => {
|
onKeyPress={e => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
handleCreateTab();
|
handleCreateTab();
|
||||||
}
|
}
|
||||||
|
|||||||
376
components/Transaction.tsx
Normal file
376
components/Transaction.tsx
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
import { Play } from "phosphor-react";
|
||||||
|
import { FC, useCallback, useEffect } from "react";
|
||||||
|
import { useSnapshot } from "valtio";
|
||||||
|
import transactionsData from "../content/transactions.json";
|
||||||
|
import state, { modifyTransaction } from "../state";
|
||||||
|
import { sendTransaction } from "../state/actions";
|
||||||
|
import Box from "./Box";
|
||||||
|
import Button from "./Button";
|
||||||
|
import Container from "./Container";
|
||||||
|
import { streamState } from "./DebugStream";
|
||||||
|
import Flex from "./Flex";
|
||||||
|
import Input from "./Input";
|
||||||
|
import Text from "./Text";
|
||||||
|
import Select from "./Select";
|
||||||
|
|
||||||
|
type TxFields = Omit<
|
||||||
|
typeof transactionsData[0],
|
||||||
|
"Account" | "Sequence" | "TransactionType"
|
||||||
|
>;
|
||||||
|
|
||||||
|
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
|
||||||
|
|
||||||
|
type SelectOption = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface TransactionState {
|
||||||
|
selectedTransaction: SelectOption | null;
|
||||||
|
selectedAccount: SelectOption | null;
|
||||||
|
selectedDestAccount: SelectOption | null;
|
||||||
|
txIsLoading: boolean;
|
||||||
|
txIsDisabled: boolean;
|
||||||
|
txFields: TxFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionProps {
|
||||||
|
header: string;
|
||||||
|
state: TransactionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Transaction: FC<TransactionProps> = ({
|
||||||
|
header,
|
||||||
|
state: {
|
||||||
|
selectedAccount,
|
||||||
|
selectedDestAccount,
|
||||||
|
selectedTransaction,
|
||||||
|
txFields,
|
||||||
|
txIsDisabled,
|
||||||
|
txIsLoading,
|
||||||
|
},
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { accounts } = useSnapshot(state);
|
||||||
|
|
||||||
|
const setState = useCallback(
|
||||||
|
(pTx?: Partial<TransactionState>) => {
|
||||||
|
modifyTransaction(header, pTx);
|
||||||
|
},
|
||||||
|
[header]
|
||||||
|
);
|
||||||
|
|
||||||
|
const transactionsOptions = transactionsData.map(tx => ({
|
||||||
|
value: tx.TransactionType,
|
||||||
|
label: tx.TransactionType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const transactionType = selectedTransaction?.value;
|
||||||
|
const account = accounts.find(
|
||||||
|
acc => acc.address === selectedAccount?.value
|
||||||
|
);
|
||||||
|
if (!account || !transactionType || txIsLoading) {
|
||||||
|
setState({ txIsDisabled: true });
|
||||||
|
} else {
|
||||||
|
setState({ txIsDisabled: false });
|
||||||
|
}
|
||||||
|
}, [txIsLoading, selectedTransaction, selectedAccount, accounts, setState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let _txFields: TxFields | undefined = transactionsData.find(
|
||||||
|
tx => tx.TransactionType === selectedTransaction?.value
|
||||||
|
);
|
||||||
|
if (!_txFields) return setState({ txFields: {} });
|
||||||
|
_txFields = { ..._txFields } as TxFields;
|
||||||
|
|
||||||
|
if (!_txFields.Destination) setState({ selectedDestAccount: null });
|
||||||
|
// @ts-ignore
|
||||||
|
delete _txFields.TransactionType;
|
||||||
|
// @ts-ignore
|
||||||
|
delete _txFields.Account;
|
||||||
|
// @ts-ignore
|
||||||
|
delete _txFields.Sequence;
|
||||||
|
setState({ txFields: _txFields });
|
||||||
|
}, [setState, selectedTransaction]);
|
||||||
|
|
||||||
|
const submitTest = useCallback(async () => {
|
||||||
|
const account = accounts.find(
|
||||||
|
acc => acc.address === selectedAccount?.value
|
||||||
|
);
|
||||||
|
const TransactionType = selectedTransaction?.value;
|
||||||
|
if (!account || !TransactionType || txIsDisabled) return;
|
||||||
|
|
||||||
|
setState({ txIsLoading: true });
|
||||||
|
// setTxIsError(null)
|
||||||
|
try {
|
||||||
|
let options = { ...txFields };
|
||||||
|
|
||||||
|
options.Destination = selectedDestAccount?.value;
|
||||||
|
(Object.keys(options) as (keyof TxFields)[]).forEach(field => {
|
||||||
|
let _value = options[field];
|
||||||
|
// convert currency
|
||||||
|
if (typeof _value === "object" && _value.type === "currency") {
|
||||||
|
if (+_value.value) {
|
||||||
|
options[field] = (+_value.value * 1000000 + "") as any;
|
||||||
|
} else {
|
||||||
|
options[field] = undefined; // 👇 💀
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// handle type: `json`
|
||||||
|
if (typeof _value === "object" && _value.type === "json") {
|
||||||
|
if (typeof _value.value === "object") {
|
||||||
|
options[field] = _value.value as any;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
options[field] = JSON.parse(_value.value);
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Input error for json field '${field}': ${
|
||||||
|
error instanceof Error ? error.message : ""
|
||||||
|
}`;
|
||||||
|
throw Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete unneccesary fields
|
||||||
|
if (!options[field]) {
|
||||||
|
delete options[field];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
|
||||||
|
await sendTransaction(
|
||||||
|
account,
|
||||||
|
{
|
||||||
|
TransactionType,
|
||||||
|
...options,
|
||||||
|
},
|
||||||
|
{ logPrefix }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
state.transactionLogs.push({ type: "error", message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState({ txIsLoading: false });
|
||||||
|
}, [
|
||||||
|
header,
|
||||||
|
setState,
|
||||||
|
selectedAccount?.value,
|
||||||
|
selectedDestAccount?.value,
|
||||||
|
selectedTransaction?.value,
|
||||||
|
accounts,
|
||||||
|
txFields,
|
||||||
|
txIsDisabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const resetState = useCallback(() => {
|
||||||
|
setState({});
|
||||||
|
}, [setState]);
|
||||||
|
|
||||||
|
const handleSetAccount = (acc: SelectOption) => {
|
||||||
|
setState({ selectedAccount: acc });
|
||||||
|
streamState.selectedAccount = acc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
|
||||||
|
const otherFields = Object.keys(txFields).filter(
|
||||||
|
k => !usualFields.includes(k)
|
||||||
|
) as OtherFields;
|
||||||
|
return (
|
||||||
|
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
||||||
|
<Container
|
||||||
|
css={{
|
||||||
|
p: "$3 01",
|
||||||
|
fontSize: "$sm",
|
||||||
|
height: "calc(100% - 45px)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
fluid
|
||||||
|
css={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: "$3",
|
||||||
|
mt: "1px",
|
||||||
|
pr: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text muted css={{ mr: "$3" }}>
|
||||||
|
Transaction type:{" "}
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
instanceId="transactionsType"
|
||||||
|
placeholder="Select transaction type"
|
||||||
|
options={transactionsOptions}
|
||||||
|
hideSelectedOptions
|
||||||
|
css={{ width: "70%" }}
|
||||||
|
value={selectedTransaction}
|
||||||
|
onChange={(tx: any) => setState({ selectedTransaction: tx })}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
fluid
|
||||||
|
css={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: "$3",
|
||||||
|
pr: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text muted css={{ mr: "$3" }}>
|
||||||
|
Account:{" "}
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
instanceId="from-account"
|
||||||
|
placeholder="Select your account"
|
||||||
|
css={{ width: "70%" }}
|
||||||
|
options={accountOptions}
|
||||||
|
value={selectedAccount}
|
||||||
|
onChange={(acc: any) => handleSetAccount(acc)} // TODO make react-select have correct types for acc
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
{txFields.Amount !== undefined && (
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
fluid
|
||||||
|
css={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: "$3",
|
||||||
|
pr: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text muted css={{ mr: "$3" }}>
|
||||||
|
Amount (XRP):{" "}
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={txFields.Amount.value}
|
||||||
|
onChange={e =>
|
||||||
|
setState({
|
||||||
|
txFields: {
|
||||||
|
...txFields,
|
||||||
|
Amount: { type: "currency", value: e.target.value },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
css={{ width: "70%", flex: "inherit" }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{txFields.Destination !== undefined && (
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
fluid
|
||||||
|
css={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: "$3",
|
||||||
|
pr: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text muted css={{ mr: "$3" }}>
|
||||||
|
Destination account:{" "}
|
||||||
|
</Text>
|
||||||
|
<Select
|
||||||
|
instanceId="to-account"
|
||||||
|
placeholder="Select the destination account"
|
||||||
|
css={{ width: "70%" }}
|
||||||
|
options={destAccountOptions}
|
||||||
|
value={selectedDestAccount}
|
||||||
|
isClearable
|
||||||
|
onChange={(acc: any) => setState({ selectedDestAccount: acc })}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
{otherFields.map(field => {
|
||||||
|
let _value = txFields[field];
|
||||||
|
let value = typeof _value === "object" ? _value.value : _value;
|
||||||
|
value =
|
||||||
|
typeof value === "object"
|
||||||
|
? JSON.stringify(value)
|
||||||
|
: value?.toLocaleString();
|
||||||
|
let isCurrency =
|
||||||
|
typeof _value === "object" && _value.type === "currency";
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
key={field}
|
||||||
|
row
|
||||||
|
fluid
|
||||||
|
css={{
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: "$3",
|
||||||
|
pr: "1px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text muted css={{ mr: "$3" }}>
|
||||||
|
{field + (isCurrency ? " (XRP)" : "")}:{" "}
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChange={e =>
|
||||||
|
setState({
|
||||||
|
txFields: {
|
||||||
|
...txFields,
|
||||||
|
[field]:
|
||||||
|
typeof _value === "object"
|
||||||
|
? { ..._value, value: e.target.value }
|
||||||
|
: e.target.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
css={{ width: "70%", flex: "inherit" }}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Container>
|
||||||
|
<Flex
|
||||||
|
row
|
||||||
|
css={{
|
||||||
|
justifyContent: "space-between",
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button outline>VIEW AS JSON</Button>
|
||||||
|
<Flex row>
|
||||||
|
<Button onClick={resetState} outline css={{ mr: "$3" }}>
|
||||||
|
RESET
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={submitTest}
|
||||||
|
isLoading={txIsLoading}
|
||||||
|
disabled={txIsDisabled}
|
||||||
|
>
|
||||||
|
<Play weight="bold" size="16px" />
|
||||||
|
RUN TEST
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Transaction;
|
||||||
@@ -4,7 +4,7 @@ export { default as Container } from "./Container";
|
|||||||
export { default as Heading } from "./Heading";
|
export { default as Heading } from "./Heading";
|
||||||
export { default as Stack } from "./Stack";
|
export { default as Stack } from "./Stack";
|
||||||
export { default as Text } from "./Text";
|
export { default as Text } from "./Text";
|
||||||
export { default as Input } from "./Input";
|
export { default as Input, Label } from "./Input";
|
||||||
export { default as Select } from "./Select";
|
export { default as Select } from "./Select";
|
||||||
export * from "./Tabs";
|
export * from "./Tabs";
|
||||||
export * from "./AlertDialog";
|
export * from "./AlertDialog";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@radix-ui/react-dialog": "^0.1.1",
|
"@radix-ui/react-dialog": "^0.1.1",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.1.1",
|
"@radix-ui/react-dropdown-menu": "^0.1.1",
|
||||||
"@radix-ui/react-id": "^0.1.1",
|
"@radix-ui/react-id": "^0.1.1",
|
||||||
|
"@radix-ui/react-label": "^0.1.5",
|
||||||
"@radix-ui/react-tooltip": "^0.1.7",
|
"@radix-ui/react-tooltip": "^0.1.7",
|
||||||
"@stitches/react": "^1.2.6-0",
|
"@stitches/react": "^1.2.6-0",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
|
|||||||
@@ -1,23 +1,11 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { Play } from "phosphor-react";
|
|
||||||
import { FC, useCallback, useEffect, useState } from "react";
|
|
||||||
import Split from "react-split";
|
import Split from "react-split";
|
||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import {
|
import { Box, Container, Flex, Tab, Tabs } from "../../components";
|
||||||
Box,
|
import Transaction from "../../components/Transaction";
|
||||||
Button,
|
|
||||||
Container,
|
|
||||||
Flex,
|
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
Tab,
|
|
||||||
Tabs,
|
|
||||||
Text,
|
|
||||||
} from "../../components";
|
|
||||||
import transactionsData from "../../content/transactions.json";
|
|
||||||
import state from "../../state";
|
import state from "../../state";
|
||||||
import { sendTransaction } from "../../state/actions";
|
|
||||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
||||||
|
import { transactionsState, modifyTransaction } from "../../state";
|
||||||
|
|
||||||
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
|
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -30,344 +18,10 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// type SelectOption<T> = { value: T, label: string };
|
|
||||||
type TxFields = Omit<
|
|
||||||
typeof transactionsData[0],
|
|
||||||
"Account" | "Sequence" | "TransactionType"
|
|
||||||
>;
|
|
||||||
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
header?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Transaction: FC<Props> = ({ header, ...props }) => {
|
|
||||||
const snap = useSnapshot(state);
|
|
||||||
|
|
||||||
const transactionsOptions = transactionsData.map((tx) => ({
|
|
||||||
value: tx.TransactionType,
|
|
||||||
label: tx.TransactionType,
|
|
||||||
}));
|
|
||||||
const [selectedTransaction, setSelectedTransaction] = useState<
|
|
||||||
typeof transactionsOptions[0] | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const accountOptions = snap.accounts.map((acc) => ({
|
|
||||||
label: acc.name,
|
|
||||||
value: acc.address,
|
|
||||||
}));
|
|
||||||
const [selectedAccount, setSelectedAccount] = useState<
|
|
||||||
typeof accountOptions[0] | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const destAccountOptions = snap.accounts
|
|
||||||
.map((acc) => ({
|
|
||||||
label: acc.name,
|
|
||||||
value: acc.address,
|
|
||||||
}))
|
|
||||||
.filter((acc) => acc.value !== selectedAccount?.value);
|
|
||||||
const [selectedDestAccount, setSelectedDestAccount] = useState<
|
|
||||||
typeof destAccountOptions[0] | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const [txIsLoading, setTxIsLoading] = useState(false);
|
|
||||||
const [txIsDisabled, setTxIsDisabled] = useState(false);
|
|
||||||
const [txFields, setTxFields] = useState<TxFields>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const transactionType = selectedTransaction?.value;
|
|
||||||
const account = snap.accounts.find(
|
|
||||||
(acc) => acc.address === selectedAccount?.value
|
|
||||||
);
|
|
||||||
if (!account || !transactionType || txIsLoading) {
|
|
||||||
setTxIsDisabled(true);
|
|
||||||
} else {
|
|
||||||
setTxIsDisabled(false);
|
|
||||||
}
|
|
||||||
}, [txIsLoading, selectedTransaction, selectedAccount, snap.accounts]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let _txFields: TxFields | undefined = transactionsData.find(
|
|
||||||
(tx) => tx.TransactionType === selectedTransaction?.value
|
|
||||||
);
|
|
||||||
if (!_txFields) return setTxFields({});
|
|
||||||
_txFields = { ..._txFields } as TxFields;
|
|
||||||
|
|
||||||
setSelectedDestAccount(null);
|
|
||||||
// @ts-ignore
|
|
||||||
delete _txFields.TransactionType;
|
|
||||||
// @ts-ignore
|
|
||||||
delete _txFields.Account;
|
|
||||||
// @ts-ignore
|
|
||||||
delete _txFields.Sequence;
|
|
||||||
setTxFields(_txFields);
|
|
||||||
}, [selectedTransaction, setSelectedDestAccount]);
|
|
||||||
|
|
||||||
const submitTest = useCallback(async () => {
|
|
||||||
const account = snap.accounts.find(
|
|
||||||
(acc) => acc.address === selectedAccount?.value
|
|
||||||
);
|
|
||||||
const TransactionType = selectedTransaction?.value;
|
|
||||||
if (!account || !TransactionType || txIsDisabled) return;
|
|
||||||
|
|
||||||
setTxIsLoading(true);
|
|
||||||
// setTxIsError(null)
|
|
||||||
try {
|
|
||||||
let options = { ...txFields };
|
|
||||||
|
|
||||||
options.Destination = selectedDestAccount?.value;
|
|
||||||
(Object.keys(options) as (keyof TxFields)[]).forEach((field) => {
|
|
||||||
let _value = options[field];
|
|
||||||
// convert currency
|
|
||||||
if (typeof _value === "object" && _value.type === "currency") {
|
|
||||||
if (+_value.value) {
|
|
||||||
options[field] = (+_value.value * 1000000 + "") as any;
|
|
||||||
} else {
|
|
||||||
options[field] = undefined; // 👇 💀
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// handle type: `json`
|
|
||||||
if (typeof _value === "object" && _value.type === "json") {
|
|
||||||
if (typeof _value.value === "object") {
|
|
||||||
options[field] = _value.value as any;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
options[field] = JSON.parse(_value.value);
|
|
||||||
} catch (error) {
|
|
||||||
const message = `Input error for json field '${field}': ${
|
|
||||||
error instanceof Error ? error.message : ""
|
|
||||||
}`;
|
|
||||||
throw Error(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete unneccesary fields
|
|
||||||
if (!options[field]) {
|
|
||||||
delete options[field];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const logPrefix = header ? `${header.split(".")[0]}: ` : undefined;
|
|
||||||
await sendTransaction(
|
|
||||||
account,
|
|
||||||
{
|
|
||||||
TransactionType,
|
|
||||||
...options,
|
|
||||||
},
|
|
||||||
{ logPrefix }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
state.transactionLogs.push({ type: "error", message: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setTxIsLoading(false);
|
|
||||||
}, [
|
|
||||||
header,
|
|
||||||
selectedAccount?.value,
|
|
||||||
selectedDestAccount?.value,
|
|
||||||
selectedTransaction?.value,
|
|
||||||
snap.accounts,
|
|
||||||
txFields,
|
|
||||||
txIsDisabled,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
|
||||||
setSelectedAccount(null);
|
|
||||||
setSelectedDestAccount(null);
|
|
||||||
setSelectedTransaction(null);
|
|
||||||
setTxFields({});
|
|
||||||
setTxIsDisabled(false);
|
|
||||||
setTxIsLoading(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
|
|
||||||
const otherFields = Object.keys(txFields).filter(
|
|
||||||
(k) => !usualFields.includes(k)
|
|
||||||
) as OtherFields;
|
|
||||||
return (
|
|
||||||
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
|
||||||
<Container
|
|
||||||
css={{
|
|
||||||
p: "$3 01",
|
|
||||||
fontSize: "$sm",
|
|
||||||
height: "calc(100% - 45px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
|
|
||||||
<Flex
|
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
mt: "1px",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Transaction type:{" "}
|
|
||||||
</Text>
|
|
||||||
<Select
|
|
||||||
instanceId="transactionsType"
|
|
||||||
placeholder="Select transaction type"
|
|
||||||
options={transactionsOptions}
|
|
||||||
hideSelectedOptions
|
|
||||||
css={{ width: "70%" }}
|
|
||||||
value={selectedTransaction}
|
|
||||||
onChange={(tt) => setSelectedTransaction(tt as any)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Flex
|
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Account:{" "}
|
|
||||||
</Text>
|
|
||||||
<Select
|
|
||||||
instanceId="from-account"
|
|
||||||
placeholder="Select your account"
|
|
||||||
css={{ width: "70%" }}
|
|
||||||
options={accountOptions}
|
|
||||||
value={selectedAccount}
|
|
||||||
onChange={(acc) => setSelectedAccount(acc as any)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
{txFields.Amount !== undefined && (
|
|
||||||
<Flex
|
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Amount (XRP):{" "}
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
value={txFields.Amount.value}
|
|
||||||
onChange={(e) =>
|
|
||||||
setTxFields({
|
|
||||||
...txFields,
|
|
||||||
Amount: { type: "currency", value: e.target.value },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
css={{ width: "70%", flex: "inherit", height: "$9" }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{txFields.Destination !== undefined && (
|
|
||||||
<Flex
|
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
Destination account:{" "}
|
|
||||||
</Text>
|
|
||||||
<Select
|
|
||||||
instanceId="to-account"
|
|
||||||
placeholder="Select the destination account"
|
|
||||||
css={{ width: "70%" }}
|
|
||||||
options={destAccountOptions}
|
|
||||||
value={selectedDestAccount}
|
|
||||||
isClearable
|
|
||||||
onChange={(acc) => setSelectedDestAccount(acc as any)}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
{otherFields.map((field) => {
|
|
||||||
let _value = txFields[field];
|
|
||||||
let value = typeof _value === "object" ? _value.value : _value;
|
|
||||||
value =
|
|
||||||
typeof value === "object"
|
|
||||||
? JSON.stringify(value)
|
|
||||||
: value?.toLocaleString();
|
|
||||||
let isCurrency =
|
|
||||||
typeof _value === "object" && _value.type === "currency";
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
key={field}
|
|
||||||
row
|
|
||||||
fluid
|
|
||||||
css={{
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
alignItems: "center",
|
|
||||||
mb: "$3",
|
|
||||||
pr: "1px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text muted css={{ mr: "$3" }}>
|
|
||||||
{field + (isCurrency ? " (XRP)" : "")}:{" "}
|
|
||||||
</Text>
|
|
||||||
<Input
|
|
||||||
value={value}
|
|
||||||
onChange={(e) =>
|
|
||||||
setTxFields({
|
|
||||||
...txFields,
|
|
||||||
[field]:
|
|
||||||
typeof _value === "object"
|
|
||||||
? { ..._value, value: e.target.value }
|
|
||||||
: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
css={{ width: "70%", flex: "inherit", height: "$9" }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
<Flex
|
|
||||||
row
|
|
||||||
css={{
|
|
||||||
justifyContent: "space-between",
|
|
||||||
position: "absolute",
|
|
||||||
left: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button outline>VIEW AS JSON</Button>
|
|
||||||
<Flex row>
|
|
||||||
<Button onClick={resetState} outline css={{ mr: "$3" }}>
|
|
||||||
RESET
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={submitTest}
|
|
||||||
isLoading={txIsLoading}
|
|
||||||
disabled={txIsDisabled}
|
|
||||||
>
|
|
||||||
<Play weight="bold" size="16px" />
|
|
||||||
RUN TEST
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
const { transactionLogs } = useSnapshot(state);
|
const { transactionLogs } = useSnapshot(state);
|
||||||
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
|
const { transactions, activeHeader } = useSnapshot(transactionsState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container css={{ px: 0 }}>
|
<Container css={{ px: 0 }}>
|
||||||
<Split
|
<Split
|
||||||
@@ -376,7 +30,7 @@ const Test = () => {
|
|||||||
gutterSize={4}
|
gutterSize={4}
|
||||||
gutterAlign="center"
|
gutterAlign="center"
|
||||||
style={{ height: "calc(100vh - 60px)" }}
|
style={{ height: "calc(100vh - 60px)" }}
|
||||||
onDragEnd={(e) => saveSplit("testVertical", e)}
|
onDragEnd={e => saveSplit("testVertical", e)}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
row
|
row
|
||||||
@@ -398,23 +52,29 @@ const Test = () => {
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
}}
|
}}
|
||||||
onDragEnd={(e) => saveSplit("testHorizontal", e)}
|
onDragEnd={e => saveSplit("testHorizontal", e)}
|
||||||
>
|
>
|
||||||
<Box css={{ width: "55%", px: "$2" }}>
|
<Box css={{ width: "55%", px: "$2" }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
activeHeader={activeHeader}
|
||||||
|
// TODO make header a required field
|
||||||
|
onChangeActive={(idx, header) => {
|
||||||
|
if (header) transactionsState.activeHeader = header;
|
||||||
|
}}
|
||||||
keepAllAlive
|
keepAllAlive
|
||||||
forceDefaultExtension
|
forceDefaultExtension
|
||||||
defaultExtension=".json"
|
defaultExtension=".json"
|
||||||
onCreateNewTab={(name) =>
|
onCreateNewTab={header => modifyTransaction(header, {})}
|
||||||
setTabHeaders(tabHeaders.concat(name))
|
onCloseTab={(idx, header) =>
|
||||||
}
|
header && modifyTransaction(header, undefined)
|
||||||
onCloseTab={(index) =>
|
|
||||||
setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{tabHeaders.map((header) => (
|
{transactions.map(({ header, state }) => (
|
||||||
<Tab key={header} header={header}>
|
<Tab key={header} header={header}>
|
||||||
<Transaction header={header} />
|
<Transaction
|
||||||
|
state={state}
|
||||||
|
header={header}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -159,3 +159,5 @@ if (typeof window !== "undefined") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
export default state
|
export default state
|
||||||
|
|
||||||
|
export * from './transactions'
|
||||||
|
|||||||
59
state/transactions.ts
Normal file
59
state/transactions.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { proxy } from 'valtio';
|
||||||
|
import { TransactionState } from '../components/Transaction';
|
||||||
|
import { deepEqual } from '../utils/object';
|
||||||
|
|
||||||
|
export const defaultTransaction: TransactionState = {
|
||||||
|
selectedTransaction: null,
|
||||||
|
selectedAccount: null,
|
||||||
|
selectedDestAccount: null,
|
||||||
|
txIsLoading: false,
|
||||||
|
txIsDisabled: false,
|
||||||
|
txFields: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transactionsState = proxy({
|
||||||
|
transactions: [
|
||||||
|
{
|
||||||
|
header: "test1.json",
|
||||||
|
state: defaultTransaction,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
activeHeader: "test1.json"
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple transaction state changer
|
||||||
|
* @param header Unique key and tab name for the transaction tab
|
||||||
|
* @param partialTx partial transaction state, `{}` resets the state and `undefined` deletes the transaction
|
||||||
|
*/
|
||||||
|
export const modifyTransaction = (
|
||||||
|
header: string,
|
||||||
|
partialTx?: Partial<TransactionState>
|
||||||
|
) => {
|
||||||
|
const tx = transactionsState.transactions.find(tx => tx.header === header);
|
||||||
|
|
||||||
|
if (partialTx === undefined) {
|
||||||
|
transactionsState.transactions = transactionsState.transactions.filter(
|
||||||
|
tx => tx.header !== header
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
transactionsState.transactions.push({
|
||||||
|
header,
|
||||||
|
state: {
|
||||||
|
...defaultTransaction,
|
||||||
|
...partialTx,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(partialTx).forEach(k => {
|
||||||
|
// Typescript mess here, but is definetly safe!
|
||||||
|
const s = tx.state as any;
|
||||||
|
const p = partialTx as any;
|
||||||
|
if (!deepEqual(s[k], p[k])) s[k] = p[k];
|
||||||
|
});
|
||||||
|
};
|
||||||
24
utils/object.ts
Normal file
24
utils/object.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export const deepEqual = (object1: any, object2: any) => {
|
||||||
|
if (!isObject(object1) || !isObject(object2)) return object1 === object2
|
||||||
|
|
||||||
|
const keys1 = Object.keys(object1);
|
||||||
|
const keys2 = Object.keys(object2);
|
||||||
|
if (keys1.length !== keys2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const key of keys1) {
|
||||||
|
const val1 = object1[key];
|
||||||
|
const val2 = object2[key];
|
||||||
|
const areObjects = isObject(val1) && isObject(val2);
|
||||||
|
if (
|
||||||
|
areObjects && !deepEqual(val1, val2) ||
|
||||||
|
!areObjects && val1 !== val2
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export const isObject = (object: any) => {
|
||||||
|
return object != null && typeof object === 'object';
|
||||||
|
}
|
||||||
11
yarn.lock
11
yarn.lock
@@ -674,6 +674,17 @@
|
|||||||
"@babel/runtime" "^7.13.10"
|
"@babel/runtime" "^7.13.10"
|
||||||
"@radix-ui/react-use-layout-effect" "0.1.0"
|
"@radix-ui/react-use-layout-effect" "0.1.0"
|
||||||
|
|
||||||
|
"@radix-ui/react-label@^0.1.5":
|
||||||
|
version "0.1.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.1.5.tgz#12cd965bfc983e0148121d4c99fb8e27a917c45c"
|
||||||
|
integrity sha512-Au9+n4/DhvjR0IHhvZ1LPdx/OW+3CGDie30ZyCkbSHIuLp4/CV4oPPGBwJ1vY99Jog3zyQhsGww9MXj8O9Aj/A==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.13.10"
|
||||||
|
"@radix-ui/react-compose-refs" "0.1.0"
|
||||||
|
"@radix-ui/react-context" "0.1.1"
|
||||||
|
"@radix-ui/react-id" "0.1.5"
|
||||||
|
"@radix-ui/react-primitive" "0.1.4"
|
||||||
|
|
||||||
"@radix-ui/react-menu@0.1.6":
|
"@radix-ui/react-menu@0.1.6":
|
||||||
version "0.1.6"
|
version "0.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.1.6.tgz#7f9521a10f6a9cd819b33b33d5ed9538d79b2e75"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.1.6.tgz#7f9521a10f6a9cd819b33b33d5ed9538d79b2e75"
|
||||||
|
|||||||
Reference in New Issue
Block a user