Editable json and unified state.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Play } from "phosphor-react";
|
||||
import { FC, useCallback, useMemo, useState } from "react";
|
||||
import { FC, useCallback, useEffect, useMemo } from "react";
|
||||
import { useSnapshot } from "valtio";
|
||||
import state from "../../state";
|
||||
import {
|
||||
@@ -32,10 +32,10 @@ const Transaction: FC<TransactionProps> = ({
|
||||
txFields,
|
||||
txIsDisabled,
|
||||
txIsLoading,
|
||||
viewType,
|
||||
editorSavedValue,
|
||||
} = txState;
|
||||
|
||||
const [viewType, setViewType] = useState<"ui" | "json">("ui");
|
||||
|
||||
const setState = useCallback(
|
||||
(pTx?: Partial<TransactionState>) => {
|
||||
modifyTransaction(header, pTx);
|
||||
@@ -61,6 +61,16 @@ const Transaction: FC<TransactionProps> = ({
|
||||
txFields,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const transactionType = selectedTransaction?.value;
|
||||
const account = selectedAccount?.value;
|
||||
if (!account || !transactionType || txIsLoading) {
|
||||
setState({ txIsDisabled: true });
|
||||
} else {
|
||||
setState({ txIsDisabled: false });
|
||||
}
|
||||
}, [txIsLoading, selectedTransaction, selectedAccount, accounts, setState]);
|
||||
|
||||
const submitTest = useCallback(async () => {
|
||||
const account = accounts.find(
|
||||
acc => acc.address === selectedAccount?.value
|
||||
@@ -92,21 +102,20 @@ const Transaction: FC<TransactionProps> = ({
|
||||
]);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setState({});
|
||||
}, [setState]);
|
||||
modifyTransaction(header, { viewType }, { replaceState: true });
|
||||
}, [header, viewType]);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return JSON.stringify(
|
||||
prepareOptions?.() || {},
|
||||
null,
|
||||
editorSettings.tabSize
|
||||
);
|
||||
}, [editorSettings.tabSize, prepareOptions]);
|
||||
const jsonValue = useMemo(
|
||||
() =>
|
||||
editorSavedValue ||
|
||||
JSON.stringify(prepareOptions?.() || {}, null, editorSettings.tabSize),
|
||||
[editorSavedValue, editorSettings.tabSize, prepareOptions]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
||||
{viewType === "json" ? (
|
||||
<TxJson value={value} header={header} />
|
||||
<TxJson value={jsonValue} header={header} setState={setState} />
|
||||
) : (
|
||||
<TxUI state={txState} setState={setState} />
|
||||
)}
|
||||
@@ -122,7 +131,11 @@ const Transaction: FC<TransactionProps> = ({
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setViewType(viewType === "ui" ? "json" : "ui")}
|
||||
onClick={() => {
|
||||
if (viewType === "ui") {
|
||||
setState({ editorSavedValue: null, viewType: "json" });
|
||||
} else setState({ viewType: "ui" });
|
||||
}}
|
||||
outline
|
||||
>
|
||||
{viewType === "ui" ? "VIEW AS JSON" : "EXIT JSON VIEW"}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import Editor, { loader } from "@monaco-editor/react";
|
||||
import { FC } from 'react';
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import dark from "../../theme/editor/amy.json";
|
||||
import light from "../../theme/editor/xcode_default.json";
|
||||
import { useSnapshot } from 'valtio';
|
||||
import state from '../../state';
|
||||
import { useSnapshot } from "valtio";
|
||||
import state, { TransactionState } from "../../state";
|
||||
import Text from "../Text";
|
||||
import Flex from "../Flex";
|
||||
import { Link } from "..";
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
@@ -14,37 +17,166 @@ loader.config({
|
||||
});
|
||||
|
||||
interface JsonProps {
|
||||
value?: string
|
||||
header?: string
|
||||
value?: string;
|
||||
header?: string;
|
||||
setState: (pTx?: Partial<TransactionState> | undefined) => void;
|
||||
}
|
||||
|
||||
export const TxJson: FC<JsonProps> = ({ value, header }) => {
|
||||
function parseJSON(str: string): any | undefined {
|
||||
try {
|
||||
const parsed = JSON.parse(str);
|
||||
return typeof parsed === "object" ? parsed : undefined;
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const TxJson: FC<JsonProps> = ({ value = "", header, setState }) => {
|
||||
const { editorSettings } = useSnapshot(state);
|
||||
const { theme } = useTheme();
|
||||
const [editorValue, setEditorValue] = useState(value);
|
||||
const [hasUnsaved, setHasUnsaved] = useState(false);
|
||||
|
||||
const path = `file:///${header}`
|
||||
useEffect(() => {
|
||||
setEditorValue(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorValue === value) setHasUnsaved(false);
|
||||
else setHasUnsaved(true);
|
||||
}, [editorValue, value]);
|
||||
|
||||
const saveState = (value: string) => {
|
||||
const options = parseJSON(value);
|
||||
if (!options) return alert("Cannot save dirty editor");
|
||||
|
||||
const { Account, TransactionType, Destination, ...rest } = options;
|
||||
let tx: Partial<TransactionState> = {};
|
||||
|
||||
if (Account) {
|
||||
const acc = state.accounts.find(acc => acc.address === Account);
|
||||
if (acc) {
|
||||
tx.selectedAccount = {
|
||||
label: acc.name,
|
||||
value: acc.address,
|
||||
};
|
||||
} else {
|
||||
tx.selectedAccount = {
|
||||
label: Account,
|
||||
value: Account,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
tx.selectedAccount = null;
|
||||
}
|
||||
|
||||
if (TransactionType) {
|
||||
tx.selectedTransaction = {
|
||||
label: TransactionType,
|
||||
value: TransactionType,
|
||||
};
|
||||
} else {
|
||||
tx.selectedTransaction = null;
|
||||
}
|
||||
|
||||
if (Destination) {
|
||||
const dest = state.accounts.find(acc => acc.address === Destination);
|
||||
if (dest) {
|
||||
tx.selectedDestAccount = {
|
||||
label: dest.name,
|
||||
value: dest.address,
|
||||
};
|
||||
} else {
|
||||
tx.selectedDestAccount = {
|
||||
label: Destination,
|
||||
value: Destination,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(rest).forEach(field => {
|
||||
const value = rest[field];
|
||||
console.log({ field, value });
|
||||
if (field === "Amount") {
|
||||
rest[field] = {
|
||||
type: "currency",
|
||||
value: +value / 1000000, // TODO handle object currencies
|
||||
};
|
||||
} else if (typeof value === "object") {
|
||||
rest[field] = {
|
||||
type: "json",
|
||||
value,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
tx.txFields = rest;
|
||||
tx.editorSavedValue = null;
|
||||
|
||||
setState(tx);
|
||||
};
|
||||
|
||||
const discardChanges = () => {
|
||||
let discard = confirm("Are you sure to discard these changes");
|
||||
if (discard) {
|
||||
setEditorValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
const onExit = (value: string) => {
|
||||
const options = parseJSON(value);
|
||||
if (options) {
|
||||
saveState(value);
|
||||
return;
|
||||
}
|
||||
const discard = confirm(
|
||||
`Malformed Transaction in ${header}, would you like to discard these changes?`
|
||||
);
|
||||
if (!discard) {
|
||||
setState({ viewType: "json", editorSavedValue: value });
|
||||
} else {
|
||||
setEditorValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
const path = `file:///${header}`;
|
||||
return (
|
||||
<Editor
|
||||
className="hooks-editor"
|
||||
language={"json"}
|
||||
path={path}
|
||||
height="calc(100% - 45px)"
|
||||
beforeMount={monaco => {
|
||||
monaco.editor.defineTheme("dark", dark as any);
|
||||
monaco.editor.defineTheme("light", light as any);
|
||||
}}
|
||||
value={value}
|
||||
onMount={(editor, monaco) => {
|
||||
editor.updateOptions({
|
||||
minimap: { enabled: false },
|
||||
glyphMargin: true,
|
||||
tabSize: editorSettings.tabSize,
|
||||
dragAndDrop: true,
|
||||
fontSize: 14,
|
||||
readOnly: true,
|
||||
});
|
||||
}}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
/>
|
||||
<Flex
|
||||
fluid
|
||||
column
|
||||
css={{ height: "calc(100% - 45px)", position: "relative" }}
|
||||
>
|
||||
<Editor
|
||||
className="hooks-editor"
|
||||
language={"json"}
|
||||
path={path}
|
||||
height="100%"
|
||||
beforeMount={monaco => {
|
||||
monaco.editor.defineTheme("dark", dark as any);
|
||||
monaco.editor.defineTheme("light", light as any);
|
||||
}}
|
||||
value={editorValue}
|
||||
onChange={val => setEditorValue(val || "")}
|
||||
onMount={(editor, monaco) => {
|
||||
editor.updateOptions({
|
||||
minimap: { enabled: false },
|
||||
glyphMargin: true,
|
||||
tabSize: editorSettings.tabSize,
|
||||
dragAndDrop: true,
|
||||
fontSize: 14,
|
||||
});
|
||||
const model = editor.getModel();
|
||||
model?.onWillDispose(() => onExit(model.getValue()));
|
||||
}}
|
||||
theme={theme === "dark" ? "dark" : "light"}
|
||||
/>
|
||||
{hasUnsaved && (
|
||||
<Text muted small css={{ position: "absolute", bottom: 0, right: 0 }}>
|
||||
This file has unsaved changes.{" "}
|
||||
<Link onClick={() => saveState(editorValue)}>save</Link>{" "}
|
||||
<Link onClick={discardChanges}>discard</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { FC } from "react";
|
||||
import Container from "../Container";
|
||||
import Flex from "../Flex";
|
||||
import Input from "../Input";
|
||||
@@ -27,14 +27,8 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
selectedDestAccount,
|
||||
selectedTransaction,
|
||||
txFields,
|
||||
txIsLoading,
|
||||
} = txState;
|
||||
|
||||
const handleSetAccount = (acc: SelectOption) => {
|
||||
setState({ selectedAccount: acc });
|
||||
streamState.selectedAccount = acc;
|
||||
};
|
||||
|
||||
const transactionsOptions = transactionsData.map(tx => ({
|
||||
value: tx.TransactionType,
|
||||
label: tx.TransactionType,
|
||||
@@ -52,22 +46,11 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
}))
|
||||
.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(() => {
|
||||
const resetOptions = (tt: string) => {
|
||||
const txFields: TxFields | undefined = transactionsData.find(
|
||||
tx => tx.TransactionType === selectedTransaction?.value
|
||||
tx => tx.TransactionType === tt
|
||||
);
|
||||
|
||||
if (!txFields) return setState({ txFields: {} });
|
||||
|
||||
const _txFields = Object.keys(txFields)
|
||||
@@ -79,7 +62,17 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
|
||||
if (!_txFields.Destination) setState({ selectedDestAccount: null });
|
||||
setState({ txFields: _txFields });
|
||||
}, [setState, selectedTransaction]);
|
||||
};
|
||||
|
||||
const handleSetAccount = (acc: SelectOption) => {
|
||||
setState({ selectedAccount: acc });
|
||||
streamState.selectedAccount = acc;
|
||||
};
|
||||
|
||||
const handleChangeTxType = (tt: SelectOption) => {
|
||||
setState({ selectedTransaction: tt });
|
||||
resetOptions(tt.value);
|
||||
};
|
||||
|
||||
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
|
||||
|
||||
@@ -117,7 +110,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
hideSelectedOptions
|
||||
css={{ width: "70%" }}
|
||||
value={selectedTransaction}
|
||||
onChange={(tx: any) => setState({ selectedTransaction: tx })}
|
||||
onChange={(tt: any) => handleChangeTxType(tt)}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
@@ -197,11 +190,18 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
)}
|
||||
{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 value: string | undefined;
|
||||
if (typeof _value === "object") {
|
||||
if (_value.type === "json" && typeof _value.value === "object") {
|
||||
value = JSON.stringify(_value.value);
|
||||
} else {
|
||||
value = _value.value.toString();
|
||||
}
|
||||
} else {
|
||||
value = _value?.toString();
|
||||
}
|
||||
|
||||
let isCurrency =
|
||||
typeof _value === "object" && _value.type === "currency";
|
||||
return (
|
||||
@@ -221,7 +221,7 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
</Text>
|
||||
<Input
|
||||
value={value}
|
||||
onChange={e =>
|
||||
onChange={e => {
|
||||
setState({
|
||||
txFields: {
|
||||
...txFields,
|
||||
@@ -230,8 +230,8 @@ export const TxUI: FC<UIProps> = ({ state: txState, setState }) => {
|
||||
? { ..._value, value: e.target.value }
|
||||
: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
css={{ width: "70%", flex: "inherit" }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface TransactionState {
|
||||
txIsLoading: boolean;
|
||||
txIsDisabled: boolean;
|
||||
txFields: TxFields;
|
||||
viewType: 'json' | 'ui',
|
||||
editorSavedValue: null | string
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +33,8 @@ export const defaultTransaction: TransactionState = {
|
||||
txIsLoading: false,
|
||||
txIsDisabled: false,
|
||||
txFields: {},
|
||||
viewType: 'ui',
|
||||
editorSavedValue: null
|
||||
};
|
||||
|
||||
export const transactionsState = proxy({
|
||||
@@ -46,11 +50,13 @@ export const transactionsState = proxy({
|
||||
/**
|
||||
* 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
|
||||
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
||||
*
|
||||
*/
|
||||
export const modifyTransaction = (
|
||||
header: string,
|
||||
partialTx?: Partial<TransactionState>
|
||||
partialTx?: Partial<TransactionState>,
|
||||
opts: { replaceState?: boolean } = {}
|
||||
) => {
|
||||
const tx = transactionsState.transactions.find(tx => tx.header === header);
|
||||
|
||||
@@ -72,9 +78,13 @@ export const modifyTransaction = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (deepEqual(partialTx, {})) {
|
||||
tx.state = { ...defaultTransaction }
|
||||
console.log({ tx: tx.state, is: tx.state === defaultTransaction })
|
||||
if (opts.replaceState) {
|
||||
const repTx: TransactionState = {
|
||||
...defaultTransaction,
|
||||
...partialTx,
|
||||
}
|
||||
tx.state = repTx
|
||||
return
|
||||
}
|
||||
|
||||
Object.keys(partialTx).forEach(k => {
|
||||
@@ -109,7 +119,8 @@ export const prepareTransaction = (data: any) => {
|
||||
} catch (error) {
|
||||
const message = `Input error for json field '${field}': ${error instanceof Error ? error.message : ""
|
||||
}`;
|
||||
throw Error(message);
|
||||
console.error(message)
|
||||
options[field] = _value.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user