Editable json and unified state.

This commit is contained in:
muzam1l
2022-04-27 18:34:28 +05:30
parent 56a9806b70
commit ab1f45febd
4 changed files with 236 additions and 80 deletions

View File

@@ -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"}

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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
}
}
}