Compare commits
16 Commits
fix/compil
...
feat/deplo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2228fb35 | ||
|
|
ca52a5e064 | ||
|
|
df0f8abe62 | ||
|
|
a6c4db1951 | ||
|
|
1c91003164 | ||
|
|
66be0efbbd | ||
|
|
9ab64ec062 | ||
|
|
e77a5e234f | ||
|
|
d2f618512a | ||
|
|
f5063de2c9 | ||
|
|
1ee8dcb536 | ||
|
|
7f6f9c11db | ||
|
|
b2b7059774 | ||
|
|
41ba096ef9 | ||
|
|
b9da659f83 | ||
|
|
6a3ff3e1d7 |
@@ -15,37 +15,11 @@ import {
|
||||
ContextMenuTriggerItem,
|
||||
} from "./primitive";
|
||||
|
||||
[
|
||||
{
|
||||
label: "Show bookmarks",
|
||||
type: "checkbox",
|
||||
checked: true,
|
||||
indicator: "*",
|
||||
onCheckedChange: () => {},
|
||||
},
|
||||
{
|
||||
type: "radio",
|
||||
label: "People",
|
||||
value: "pedro",
|
||||
onValueChange: () => {},
|
||||
options: [
|
||||
{
|
||||
value: "pedro",
|
||||
label: "Pedro Duarte",
|
||||
},
|
||||
{
|
||||
value: "colm",
|
||||
label: "Colm Tuite",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export type TextOption = {
|
||||
type: "text";
|
||||
label: ReactNode;
|
||||
onClick?: () => any;
|
||||
children?: Option[];
|
||||
onSelect?: () => any;
|
||||
children?: ContentMenuOption[];
|
||||
};
|
||||
export type SeparatorOption = { type: "separator" };
|
||||
export type CheckboxOption = {
|
||||
@@ -64,11 +38,16 @@ export type RadioOption<T extends string = string> = {
|
||||
|
||||
type WithCommons = { key: string; disabled?: boolean };
|
||||
|
||||
type Option = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
|
||||
export type ContentMenuOption = (
|
||||
| TextOption
|
||||
| SeparatorOption
|
||||
| CheckboxOption
|
||||
| RadioOption
|
||||
) &
|
||||
WithCommons;
|
||||
|
||||
interface IContextMenu {
|
||||
options?: Option[];
|
||||
export interface IContextMenu {
|
||||
options?: ContentMenuOption[];
|
||||
isNested?: boolean;
|
||||
}
|
||||
export const ContextMenu: FC<IContextMenu> = ({
|
||||
@@ -83,11 +62,11 @@ export const ContextMenu: FC<IContextMenu> = ({
|
||||
) : (
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
)}
|
||||
{options && (
|
||||
{options && !!options.length && (
|
||||
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
|
||||
{options.map(({ key, ...option }) => {
|
||||
if (option.type === "text") {
|
||||
const { children, label } = option;
|
||||
const { children, label, onSelect } = option;
|
||||
if (children)
|
||||
return (
|
||||
<ContextMenu isNested key={key} options={children}>
|
||||
@@ -97,7 +76,11 @@ export const ContextMenu: FC<IContextMenu> = ({
|
||||
</Flex>
|
||||
</ContextMenu>
|
||||
);
|
||||
return <ContextMenuItem key={key}>{label}</ContextMenuItem>;
|
||||
return (
|
||||
<ContextMenuItem key={key} onSelect={onSelect}>
|
||||
{label}
|
||||
</ContextMenuItem>
|
||||
);
|
||||
}
|
||||
if (option.type === "checkbox") {
|
||||
const { label, checked, onCheckedChange } = option;
|
||||
|
||||
@@ -15,7 +15,7 @@ const contentShow = keyframes({
|
||||
"100%": { opacity: 1 },
|
||||
});
|
||||
const StyledOverlay = styled(DialogPrimitive.Overlay, {
|
||||
zIndex: 9999,
|
||||
zIndex: 10000,
|
||||
backgroundColor: blackA.blackA9,
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
|
||||
@@ -22,6 +22,7 @@ import docs from "../xrpl-hooks-docs/docs";
|
||||
import Monaco from "./Monaco";
|
||||
import { saveAllFiles } from "../state/actions/saveFile";
|
||||
import { Tab, Tabs } from "./Tabs";
|
||||
import { renameFile } from "../state/actions/createNewFile";
|
||||
|
||||
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
const currPath = editor.getModel()?.uri.path;
|
||||
@@ -129,6 +130,7 @@ const HooksEditor = () => {
|
||||
extensionRequired
|
||||
onCreateNewTab={createNewFile}
|
||||
onCloseTab={idx => state.files.splice(idx, 1)}
|
||||
onRenameTab={(idx, nwName, oldName = "") => renameFile(oldName, nwName)}
|
||||
headerExtraValidation={{
|
||||
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
|
||||
error:
|
||||
|
||||
@@ -91,7 +91,7 @@ const Select = forwardRef<any, Props>((props, ref) => {
|
||||
...provided,
|
||||
color: colors.searchText,
|
||||
backgroundColor:
|
||||
state.isSelected || state.isFocused
|
||||
state.isFocused
|
||||
? colors.activeLight
|
||||
: colors.dropDownBg,
|
||||
":hover": {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Plus, Trash, X } from "phosphor-react";
|
||||
import Button from "./Button";
|
||||
import Box from "./Box";
|
||||
import { Button, Box, Text } from ".";
|
||||
import { Stack, Flex, Select } from ".";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -19,48 +18,30 @@ import {
|
||||
useForm,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { TTS, tts } from "../utils/hookOnCalculator";
|
||||
import { deployHook } from "../state/actions";
|
||||
import { useSnapshot } from "valtio";
|
||||
import state, { IFile, SelectOption } from "../state";
|
||||
import toast from "react-hot-toast";
|
||||
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
|
||||
import estimateFee from "../utils/estimateFee";
|
||||
|
||||
const transactionOptions = Object.keys(tts).map(key => ({
|
||||
label: key,
|
||||
value: key as keyof TTS,
|
||||
}));
|
||||
|
||||
export type SetHookData = {
|
||||
Invoke: {
|
||||
value: keyof TTS;
|
||||
label: string;
|
||||
}[];
|
||||
Fee: string;
|
||||
HookNamespace: string;
|
||||
HookParameters: {
|
||||
HookParameter: {
|
||||
HookParameterName: string;
|
||||
HookParameterValue: string;
|
||||
};
|
||||
}[];
|
||||
// HookGrants: {
|
||||
// HookGrant: {
|
||||
// Authorize: string;
|
||||
// HookHash: string;
|
||||
// };
|
||||
// }[];
|
||||
};
|
||||
import {
|
||||
getParameters,
|
||||
getInvokeOptions,
|
||||
transactionOptions,
|
||||
SetHookData,
|
||||
} from "../utils/setHook";
|
||||
import { capitalize } from "../utils/helpers";
|
||||
|
||||
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
({ accountAddress }) => {
|
||||
const snap = useSnapshot(state);
|
||||
|
||||
const [estimateLoading, setEstimateLoading] = useState(false);
|
||||
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
||||
|
||||
const compiledFiles = snap.files.filter(file => file.compiledContent);
|
||||
const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
|
||||
|
||||
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
|
||||
|
||||
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
|
||||
label: acc.name,
|
||||
value: acc.address,
|
||||
@@ -75,12 +56,23 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
|
||||
const getHookNamespace = useCallback(
|
||||
() =>
|
||||
activeFile && snap.deployValues[activeFile.name]
|
||||
? snap.deployValues[activeFile.name].HookNamespace
|
||||
: activeFile?.name.split(".")[0] || "",
|
||||
(activeFile && snap.deployValues[activeFile.name]?.HookNamespace) ||
|
||||
activeFile?.name.split(".")[0] ||
|
||||
"",
|
||||
[activeFile, snap.deployValues]
|
||||
);
|
||||
|
||||
const getDefaultValues = useCallback((): Partial<SetHookData> => {
|
||||
const content = activeFile?.compiledValueSnapshot;
|
||||
return (
|
||||
(activeFile && snap.deployValues[activeFile.name]) || {
|
||||
HookNamespace: getHookNamespace(),
|
||||
Invoke: getInvokeOptions(content),
|
||||
HookParameters: getParameters(content),
|
||||
}
|
||||
);
|
||||
}, [activeFile, getHookNamespace, snap.deployValues]);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@@ -88,29 +80,25 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
watch,
|
||||
setValue,
|
||||
getValues,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<SetHookData>({
|
||||
defaultValues: (activeFile && snap.deployValues[activeFile.name]) || {
|
||||
HookNamespace: activeFile?.name.split(".")[0] || "",
|
||||
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
|
||||
},
|
||||
defaultValues: getDefaultValues(),
|
||||
});
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "HookParameters", // unique name for your Field Array
|
||||
});
|
||||
const [formInitialized, setFormInitialized] = useState(false);
|
||||
const [estimateLoading, setEstimateLoading] = useState(false);
|
||||
|
||||
const watchedFee = watch("Fee");
|
||||
|
||||
// Update value if activeFile changes
|
||||
// Reset form if activeFile changes
|
||||
useEffect(() => {
|
||||
if (!activeFile) return;
|
||||
const defaultValue = getHookNamespace();
|
||||
const defaultValues = getDefaultValues();
|
||||
|
||||
setValue("HookNamespace", defaultValue);
|
||||
setFormInitialized(true);
|
||||
}, [setValue, activeFile, snap.deployValues, getHookNamespace]);
|
||||
reset(defaultValues);
|
||||
}, [activeFile, getDefaultValues, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -141,23 +129,19 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
calculateHashedValue();
|
||||
}, [namespace, calculateHashedValue]);
|
||||
|
||||
// Calculate initial fee estimate when modal opens
|
||||
useEffect(() => {
|
||||
if (formInitialized && account) {
|
||||
(async () => {
|
||||
const formValues = getValues();
|
||||
const tx = await prepareDeployHookTx(account, formValues);
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
const res = await estimateFee(tx, account);
|
||||
if (res && res.base_fee) {
|
||||
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
|
||||
}
|
||||
})();
|
||||
const calculateFee = useCallback(async () => {
|
||||
if (!account) return;
|
||||
|
||||
const formValues = getValues();
|
||||
const tx = await prepareDeployHookTx(account, formValues);
|
||||
if (!tx) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formInitialized]);
|
||||
const res = await estimateFee(tx, account);
|
||||
if (res && res.base_fee) {
|
||||
setValue("Fee", Math.round(Number(res.base_fee || "")).toString());
|
||||
}
|
||||
}, [account, getValues, setValue]);
|
||||
|
||||
const tooLargeFile = () => {
|
||||
return Boolean(
|
||||
@@ -172,6 +156,12 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
);
|
||||
if (!account) return;
|
||||
if (currAccount) currAccount.isLoading = true;
|
||||
|
||||
data.HookParameters.forEach(param => {
|
||||
delete param.$metaData;
|
||||
return param;
|
||||
});
|
||||
|
||||
const res = await deployHook(account, data);
|
||||
if (currAccount) currAccount.isLoading = false;
|
||||
|
||||
@@ -181,8 +171,14 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
}
|
||||
toast.error(`Transaction failed! (${res?.engine_result_message})`);
|
||||
};
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
setIsSetHookDialogOpen(open);
|
||||
|
||||
if (open) calculateFee();
|
||||
}, [calculateFee]);
|
||||
return (
|
||||
<Dialog open={isSetHookDialogOpen} onOpenChange={setIsSetHookDialogOpen}>
|
||||
<Dialog open={isSetHookDialogOpen} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
ghost
|
||||
@@ -206,7 +202,6 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
<Select
|
||||
instanceId="deploy-account"
|
||||
placeholder="Select account"
|
||||
hideSelectedOptions
|
||||
options={accountOptions}
|
||||
value={selectedAccount}
|
||||
onChange={(acc: any) => setSelectedAccount(acc)}
|
||||
@@ -252,22 +247,39 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
|
||||
<Stack>
|
||||
{fields.map((field, index) => (
|
||||
<Stack key={field.id}>
|
||||
<Input
|
||||
// important to include key with field's id
|
||||
placeholder="Parameter name"
|
||||
{...register(
|
||||
`HookParameters.${index}.HookParameter.HookParameterName`
|
||||
<Flex column>
|
||||
<Flex row>
|
||||
<Input
|
||||
// important to include key with field's id
|
||||
placeholder="Parameter name"
|
||||
readOnly={field.$metaData?.required}
|
||||
{...register(
|
||||
`HookParameters.${index}.HookParameter.HookParameterName`
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
css={{ mx: "$2" }}
|
||||
placeholder="Value (hex-quoted)"
|
||||
{...register(
|
||||
`HookParameters.${index}.HookParameter.HookParameterValue`,
|
||||
{ required: field.$metaData?.required }
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => remove(index)}
|
||||
variant="destroy"
|
||||
>
|
||||
<Trash weight="regular" size="16px" />
|
||||
</Button>
|
||||
</Flex>
|
||||
{errors.HookParameters?.[index]?.HookParameter
|
||||
?.HookParameterValue?.type === "required" && (
|
||||
<Text error>This field is required</Text>
|
||||
)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Value (hex-quoted)"
|
||||
{...register(
|
||||
`HookParameters.${index}.HookParameter.HookParameterValue`
|
||||
)}
|
||||
/>
|
||||
<Button onClick={() => remove(index)} variant="destroy">
|
||||
<Trash weight="regular" size="16px" />
|
||||
</Button>
|
||||
<Label css={{ fontSize: "$sm", mt: "$1" }}>
|
||||
{capitalize(field.$metaData?.description)}
|
||||
</Label>
|
||||
</Flex>
|
||||
</Stack>
|
||||
))}
|
||||
<Button
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, {
|
||||
useCallback,
|
||||
} from "react";
|
||||
import type { ReactNode, ReactElement } from "react";
|
||||
import { Box, Button, Flex, Input, Label, Stack, Text } from ".";
|
||||
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from ".";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { Plus, X } from "phosphor-react";
|
||||
import { styled } from "../stitches.config";
|
||||
import { capitalize } from "../utils/helpers";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import ContextMenu, { ContentMenuOption } from "./ContextMenu";
|
||||
|
||||
const ErrorText = styled(Text, {
|
||||
color: "$error",
|
||||
@@ -26,6 +26,8 @@ const ErrorText = styled(Text, {
|
||||
display: "block",
|
||||
});
|
||||
|
||||
type Nullable<T> = T | null | undefined | false;
|
||||
|
||||
interface TabProps {
|
||||
header: string;
|
||||
children?: ReactNode;
|
||||
@@ -47,6 +49,7 @@ interface Props {
|
||||
error: string;
|
||||
};
|
||||
onCreateNewTab?: (name: string) => any;
|
||||
onRenameTab?: (index: number, nwName: string, oldName?: string) => any;
|
||||
onCloseTab?: (index: number, header?: string) => any;
|
||||
onChangeActive?: (index: number, header?: string) => any;
|
||||
}
|
||||
@@ -63,6 +66,7 @@ export const Tabs = ({
|
||||
onCreateNewTab,
|
||||
onCloseTab,
|
||||
onChangeActive,
|
||||
onRenameTab,
|
||||
headerExtraValidation,
|
||||
extensionRequired,
|
||||
defaultExtension = "",
|
||||
@@ -72,8 +76,9 @@ export const Tabs = ({
|
||||
const tabs: TabProps[] = children.map(elem => elem.props);
|
||||
|
||||
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
||||
const [renamingTab, setRenamingTab] = useState<number | null>(null);
|
||||
const [tabname, setTabname] = useState("");
|
||||
const [newtabError, setNewtabError] = useState<string | null>(null);
|
||||
const [tabnameError, setTabnameError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeIndex) setActive(activeIndex);
|
||||
@@ -89,21 +94,24 @@ export const Tabs = ({
|
||||
|
||||
// when filename changes, reset error
|
||||
useEffect(() => {
|
||||
setNewtabError(null);
|
||||
}, [tabname, setNewtabError]);
|
||||
setTabnameError(null);
|
||||
}, [tabname, setTabnameError]);
|
||||
|
||||
const validateTabname = useCallback(
|
||||
(tabname: string): { error: string | null } => {
|
||||
(tabname: string): { error?: string, result?: string } => {
|
||||
if (!tabname) {
|
||||
return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
|
||||
}
|
||||
let ext =
|
||||
(tabname.includes(".") && tabname.split(".").pop()) || "";
|
||||
|
||||
if (!ext && defaultExtension) {
|
||||
ext = defaultExtension
|
||||
tabname = `${tabname}.${defaultExtension}`
|
||||
}
|
||||
if (tabs.find(tab => tab.header === tabname)) {
|
||||
return { error: `${capitalize(label)} name already exists.` };
|
||||
}
|
||||
const ext =
|
||||
(tabname.includes(".") && tabname.split(".").pop()) ||
|
||||
defaultExtension ||
|
||||
"";
|
||||
if (extensionRequired && !ext) {
|
||||
return { error: "File extension is required!" };
|
||||
}
|
||||
@@ -116,7 +124,7 @@ export const Tabs = ({
|
||||
) {
|
||||
return { error: headerExtraValidation.error };
|
||||
}
|
||||
return { error: null };
|
||||
return { result: tabname };
|
||||
},
|
||||
[
|
||||
allowedExtensions,
|
||||
@@ -136,16 +144,33 @@ export const Tabs = ({
|
||||
[onChangeActive]
|
||||
);
|
||||
|
||||
const handleCreateTab = useCallback(() => {
|
||||
const chk = validateTabname(tabname);
|
||||
if (chk.error) {
|
||||
setNewtabError(`Error: ${chk.error}`);
|
||||
const handleRenameTab = useCallback(() => {
|
||||
if (renamingTab === null) return;
|
||||
|
||||
const res = validateTabname(tabname);
|
||||
if (res.error) {
|
||||
setTabnameError(`Error: ${res.error}`);
|
||||
return;
|
||||
}
|
||||
let _tabname = tabname;
|
||||
if (defaultExtension && !_tabname.endsWith(defaultExtension)) {
|
||||
_tabname = `${_tabname}.${defaultExtension}`;
|
||||
|
||||
const { result: _tabname = tabname } = res
|
||||
|
||||
setRenamingTab(null);
|
||||
setTabname("");
|
||||
|
||||
const oldName = tabs[renamingTab]?.header;
|
||||
onRenameTab?.(renamingTab, _tabname, oldName);
|
||||
|
||||
handleActiveChange(renamingTab);
|
||||
}, [handleActiveChange, onRenameTab, renamingTab, tabname, tabs, validateTabname]);
|
||||
|
||||
const handleCreateTab = useCallback(() => {
|
||||
const res = validateTabname(tabname);
|
||||
if (res.error) {
|
||||
setTabnameError(`Error: ${res.error}`);
|
||||
return;
|
||||
}
|
||||
const { result: _tabname = tabname } = res
|
||||
|
||||
setIsNewtabDialogOpen(false);
|
||||
setTabname("");
|
||||
@@ -153,14 +178,7 @@ export const Tabs = ({
|
||||
onCreateNewTab?.(_tabname);
|
||||
|
||||
handleActiveChange(tabs.length, _tabname);
|
||||
}, [
|
||||
tabname,
|
||||
defaultExtension,
|
||||
validateTabname,
|
||||
onCreateNewTab,
|
||||
handleActiveChange,
|
||||
tabs.length,
|
||||
]);
|
||||
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]);
|
||||
|
||||
const handleCloseTab = useCallback(
|
||||
(idx: number) => {
|
||||
@@ -175,6 +193,21 @@ export const Tabs = ({
|
||||
[active, handleActiveChange, onCloseTab, tabs]
|
||||
);
|
||||
|
||||
const closeOption = (idx: number): Nullable<ContentMenuOption> =>
|
||||
onCloseTab && {
|
||||
type: "text",
|
||||
label: "Close",
|
||||
key: "close",
|
||||
onSelect: () => handleCloseTab(idx),
|
||||
};
|
||||
const renameOption = (idx: number): Nullable<ContentMenuOption> =>
|
||||
onRenameTab && {
|
||||
type: "text",
|
||||
label: "Rename",
|
||||
key: "rename",
|
||||
onSelect: () => setRenamingTab(idx),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!headless && (
|
||||
@@ -189,7 +222,14 @@ export const Tabs = ({
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab, idx) => (
|
||||
<ContextMenu key={tab.header}>
|
||||
<ContextMenu
|
||||
key={tab.header}
|
||||
options={
|
||||
[closeOption(idx), renameOption(idx)].filter(
|
||||
Boolean
|
||||
) as ContentMenuOption[]
|
||||
}
|
||||
>
|
||||
<Button
|
||||
role="tab"
|
||||
tabIndex={idx}
|
||||
@@ -261,7 +301,7 @@ export const Tabs = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ErrorText>{newtabError}</ErrorText>
|
||||
<ErrorText>{tabnameError}</ErrorText>
|
||||
</DialogDescription>
|
||||
|
||||
<Flex
|
||||
@@ -286,6 +326,51 @@ export const Tabs = ({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{onRenameTab && (
|
||||
<Dialog
|
||||
open={renamingTab !== null}
|
||||
onOpenChange={() => setRenamingTab(null)}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogTitle>
|
||||
Rename <Pre>{tabs[renamingTab || 0]?.header}</Pre>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Label>Enter new name</Label>
|
||||
<Input
|
||||
value={tabname}
|
||||
onChange={e => setTabname(e.target.value)}
|
||||
onKeyPress={e => {
|
||||
if (e.key === "Enter") {
|
||||
handleRenameTab();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ErrorText>{tabnameError}</ErrorText>
|
||||
</DialogDescription>
|
||||
|
||||
<Flex
|
||||
css={{
|
||||
marginTop: 25,
|
||||
justifyContent: "flex-end",
|
||||
gap: "$3",
|
||||
}}
|
||||
>
|
||||
<DialogClose asChild>
|
||||
<Button outline>Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button variant="primary" onClick={handleRenameTab}>
|
||||
Confirm
|
||||
</Button>
|
||||
</Flex>
|
||||
<DialogClose asChild>
|
||||
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
||||
<X size="20px" />
|
||||
</Box>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
{keepAllAlive
|
||||
|
||||
@@ -5,7 +5,7 @@ import state from "../../state";
|
||||
import {
|
||||
defaultTransactionType,
|
||||
getTxFields,
|
||||
modifyTransaction,
|
||||
modifyTxState,
|
||||
prepareState,
|
||||
prepareTransaction,
|
||||
SelectOption,
|
||||
@@ -42,7 +42,7 @@ const Transaction: FC<TransactionProps> = ({
|
||||
|
||||
const setState = useCallback(
|
||||
(pTx?: Partial<TransactionState>) => {
|
||||
return modifyTransaction(header, pTx);
|
||||
return modifyTxState(header, pTx);
|
||||
},
|
||||
[header]
|
||||
);
|
||||
@@ -164,7 +164,7 @@ const Transaction: FC<TransactionProps> = ({
|
||||
}
|
||||
nwState.txFields = fields;
|
||||
|
||||
const state = modifyTransaction(header, nwState, { replaceState: true });
|
||||
const state = modifyTxState(header, nwState, { replaceState: true });
|
||||
const editorValue = getJsonString(state);
|
||||
return setState({ editorValue });
|
||||
},
|
||||
|
||||
@@ -3,12 +3,12 @@ import Split from "react-split";
|
||||
import { useSnapshot } from "valtio";
|
||||
import { Box, Container, Flex, Tab, Tabs } from "../../components";
|
||||
import Transaction from "../../components/Transaction";
|
||||
import state from "../../state";
|
||||
import state, { renameTxState } from "../../state";
|
||||
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
|
||||
import { transactionsState, modifyTransaction } from "../../state";
|
||||
import { transactionsState, modifyTxState } from "../../state";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FileJs } from "phosphor-react";
|
||||
import RunScript from '../../components/RunScript';
|
||||
import RunScript from "../../components/RunScript";
|
||||
|
||||
const DebugStream = dynamic(() => import("../../components/DebugStream"), {
|
||||
ssr: false,
|
||||
@@ -96,9 +96,12 @@ const Test = () => {
|
||||
keepAllAlive
|
||||
defaultExtension="json"
|
||||
allowedExtensions={["json"]}
|
||||
onCreateNewTab={header => modifyTransaction(header, {})}
|
||||
onCreateNewTab={header => modifyTxState(header, {})}
|
||||
onRenameTab={(idx, nwName, oldName = "") =>
|
||||
renameTxState(oldName, nwName)
|
||||
}
|
||||
onCloseTab={(idx, header) =>
|
||||
header && modifyTransaction(header, undefined)
|
||||
header && modifyTxState(header, undefined)
|
||||
}
|
||||
>
|
||||
{transactions.map(({ header, state }) => (
|
||||
|
||||
@@ -28,40 +28,34 @@ export const names = [
|
||||
* is protected with CORS so that's why we did our own endpoint
|
||||
*/
|
||||
export const addFaucetAccount = async (name?: string, showToast: boolean = false) => {
|
||||
// Lets limit the number of faucet accounts to 5 for now
|
||||
if (state.accounts.length > 5) {
|
||||
return toast.error("You can only have maximum 6 accounts");
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window === undefined) return
|
||||
|
||||
|
||||
const toastId = showToast ? toast.loading("Creating account") : "";
|
||||
const res = await fetch(`${window.location.origin}/api/faucet`, {
|
||||
method: "POST",
|
||||
});
|
||||
const json: FaucetAccountRes | { error: string } = await res.json();
|
||||
if ("error" in json) {
|
||||
if (showToast) {
|
||||
return toast.error(json.error, { id: toastId });
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const toastId = showToast ? toast.loading("Creating account") : "";
|
||||
const res = await fetch(`${window.location.origin}/api/faucet`, {
|
||||
method: "POST",
|
||||
});
|
||||
const json: FaucetAccountRes | { error: string } = await res.json();
|
||||
if ("error" in json) {
|
||||
if (showToast) {
|
||||
return toast.error(json.error, { id: toastId });
|
||||
} else {
|
||||
if (showToast) {
|
||||
toast.success("New account created", { id: toastId });
|
||||
}
|
||||
const currNames = state.accounts.map(acc => acc.name);
|
||||
state.accounts.push({
|
||||
name: name || names.filter(name => !currNames.includes(name))[0],
|
||||
xrp: (json.xrp || 0 * 1000000).toString(),
|
||||
address: json.address,
|
||||
secret: json.secret,
|
||||
sequence: 1,
|
||||
hooks: [],
|
||||
isLoading: false,
|
||||
version: '2'
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (showToast) {
|
||||
toast.success("New account created", { id: toastId });
|
||||
}
|
||||
const currNames = state.accounts.map(acc => acc.name);
|
||||
state.accounts.push({
|
||||
name: name || names.filter(name => !currNames.includes(name))[0],
|
||||
xrp: (json.xrp || 0 * 1000000).toString(),
|
||||
address: json.address,
|
||||
secret: json.secret,
|
||||
sequence: 1,
|
||||
hooks: [],
|
||||
isLoading: false,
|
||||
version: '2'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,4 +14,11 @@ export const createNewFile = (name: string) => {
|
||||
const emptyFile: IFile = { name, language: languageMapping[fileExt as 'ts' | 'js' | 'md' | 'c' | 'h' | 'other'], content: "" };
|
||||
state.files.push(emptyFile);
|
||||
state.active = state.files.length - 1;
|
||||
};
|
||||
|
||||
export const renameFile = (oldName: string, nwName: string) => {
|
||||
const file = state.files.find(file => file.name === oldName)
|
||||
if (!file) throw Error(`No file exists with name ${oldName}`)
|
||||
|
||||
file.name = nwName
|
||||
};
|
||||
@@ -3,10 +3,10 @@ import toast from "react-hot-toast";
|
||||
|
||||
import state, { IAccount } from "../index";
|
||||
import calculateHookOn, { TTS } from "../../utils/hookOnCalculator";
|
||||
import { SetHookData } from "../../components/SetHookDialog";
|
||||
import { Link } from "../../components";
|
||||
import { ref } from "valtio";
|
||||
import estimateFee from "../../utils/estimateFee";
|
||||
import { SetHookData } from '../../utils/setHook';
|
||||
|
||||
export const sha256 = async (string: string) => {
|
||||
const utf8 = new TextEncoder().encode(string);
|
||||
|
||||
@@ -48,13 +48,21 @@ export const transactionsState = proxy({
|
||||
activeHeader: "test1.json"
|
||||
});
|
||||
|
||||
export const renameTxState = (oldName: string, nwName: string) => {
|
||||
const tx = transactionsState.transactions.find(tx => tx.header === oldName);
|
||||
|
||||
if (!tx) throw Error(`No transaction state exists with given header name ${oldName}`);
|
||||
|
||||
tx.header = nwName
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple transaction state changer
|
||||
* @param header Unique key and tab name for the transaction tab
|
||||
* @param partialTx partial transaction state, `undefined` deletes the transaction
|
||||
*
|
||||
*/
|
||||
export const modifyTransaction = (
|
||||
export const modifyTxState = (
|
||||
header: string,
|
||||
partialTx?: Partial<TransactionState>,
|
||||
opts: { replaceState?: boolean } = {}
|
||||
|
||||
78
utils/setHook.ts
Normal file
78
utils/setHook.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { getTags } from './comment-parser';
|
||||
import { tts, TTS } from './hookOnCalculator';
|
||||
|
||||
export const transactionOptions = Object.keys(tts).map(key => ({
|
||||
label: key,
|
||||
value: key as keyof TTS,
|
||||
}));
|
||||
|
||||
export type SetHookData = {
|
||||
Invoke: {
|
||||
value: keyof TTS;
|
||||
label: string;
|
||||
}[];
|
||||
Fee: string;
|
||||
HookNamespace: string;
|
||||
HookParameters: {
|
||||
HookParameter: {
|
||||
HookParameterName: string;
|
||||
HookParameterValue: string;
|
||||
};
|
||||
$metaData?: any;
|
||||
}[];
|
||||
// HookGrants: {
|
||||
// HookGrant: {
|
||||
// Authorize: string;
|
||||
// HookHash: string;
|
||||
// };
|
||||
// }[];
|
||||
};
|
||||
|
||||
|
||||
export const getParameters = (content?: string) => {
|
||||
const fieldTags = ["field", "param", "arg", "argument"];
|
||||
const tags = getTags(content)
|
||||
.filter(tag => fieldTags.includes(tag.tag))
|
||||
.filter(tag => !!tag.name);
|
||||
|
||||
const paramters: SetHookData["HookParameters"] = tags.map(tag => ({
|
||||
HookParameter: {
|
||||
HookParameterName: tag.name,
|
||||
HookParameterValue: tag.default || "",
|
||||
},
|
||||
$metaData: {
|
||||
description: tag.description,
|
||||
required: !tag.optional
|
||||
},
|
||||
}));
|
||||
|
||||
return paramters;
|
||||
};
|
||||
|
||||
export const getInvokeOptions = (content?: string) => {
|
||||
const invokeTags = ["invoke", "invoke-on"];
|
||||
|
||||
const options = getTags(content)
|
||||
.filter(tag => invokeTags.includes(tag.tag))
|
||||
.reduce((cumm, curr) => {
|
||||
const combined = curr.type || `${curr.name} ${curr.description}`
|
||||
const opts = combined.split(' ')
|
||||
|
||||
return cumm.concat(opts as any)
|
||||
}, [] as (keyof TTS)[])
|
||||
.filter(opt => Object.keys(tts).includes(opt))
|
||||
|
||||
|
||||
const invokeOptions: SetHookData['Invoke'] = options.map(opt => ({
|
||||
label: opt,
|
||||
value: opt
|
||||
}))
|
||||
|
||||
// default
|
||||
if (!invokeOptions.length) {
|
||||
const payment = transactionOptions.find(tx => tx.value === "ttPAYMENT")
|
||||
if (payment) return [payment]
|
||||
}
|
||||
|
||||
return invokeOptions;
|
||||
};
|
||||
Reference in New Issue
Block a user