Compare commits

..

2 Commits

Author SHA1 Message Date
muzam1l
b2b7059774 Remove account import limit. 2022-07-21 15:31:06 +05:30
muzamil
3897f2d823 Merge pull request #246 from XRPLF/feat/tabs-comp
Tabs and Context menu components.
2022-07-20 17:46:04 +05:30
9 changed files with 97 additions and 191 deletions

View File

@@ -15,11 +15,37 @@ 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;
onSelect?: () => any;
children?: ContentMenuOption[];
onClick?: () => any;
children?: Option[];
};
export type SeparatorOption = { type: "separator" };
export type CheckboxOption = {
@@ -38,16 +64,11 @@ export type RadioOption<T extends string = string> = {
type WithCommons = { key: string; disabled?: boolean };
export type ContentMenuOption = (
| TextOption
| SeparatorOption
| CheckboxOption
| RadioOption
) &
type Option = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
WithCommons;
export interface IContextMenu {
options?: ContentMenuOption[];
interface IContextMenu {
options?: Option[];
isNested?: boolean;
}
export const ContextMenu: FC<IContextMenu> = ({
@@ -62,11 +83,11 @@ export const ContextMenu: FC<IContextMenu> = ({
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && !!options.length && (
{options && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === "text") {
const { children, label, onSelect } = option;
const { children, label } = option;
if (children)
return (
<ContextMenu isNested key={key} options={children}>
@@ -76,11 +97,7 @@ export const ContextMenu: FC<IContextMenu> = ({
</Flex>
</ContextMenu>
);
return (
<ContextMenuItem key={key} onSelect={onSelect}>
{label}
</ContextMenuItem>
);
return <ContextMenuItem key={key}>{label}</ContextMenuItem>;
}
if (option.type === "checkbox") {
const { label, checked, onCheckedChange } = option;

View File

@@ -15,7 +15,7 @@ const contentShow = keyframes({
"100%": { opacity: 1 },
});
const StyledOverlay = styled(DialogPrimitive.Overlay, {
zIndex: 10000,
zIndex: 9999,
backgroundColor: blackA.blackA9,
position: "fixed",
inset: 0,

View File

@@ -22,7 +22,6 @@ 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;
@@ -130,7 +129,6 @@ 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:

View File

@@ -6,7 +6,7 @@ import React, {
useCallback,
} from "react";
import type { ReactNode, ReactElement } from "react";
import { Box, Button, Flex, Input, Label, Pre, Stack, Text } from ".";
import { Box, Button, Flex, Input, Label, 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, { ContentMenuOption } from "./ContextMenu";
import ContextMenu from "./ContextMenu";
const ErrorText = styled(Text, {
color: "$error",
@@ -26,8 +26,6 @@ const ErrorText = styled(Text, {
display: "block",
});
type Nullable<T> = T | null | undefined | false;
interface TabProps {
header: string;
children?: ReactNode;
@@ -49,7 +47,6 @@ 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;
}
@@ -66,7 +63,6 @@ export const Tabs = ({
onCreateNewTab,
onCloseTab,
onChangeActive,
onRenameTab,
headerExtraValidation,
extensionRequired,
defaultExtension = "",
@@ -76,9 +72,8 @@ 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 [tabnameError, setTabnameError] = useState<string | null>(null);
const [newtabError, setNewtabError] = useState<string | null>(null);
useEffect(() => {
if (activeIndex) setActive(activeIndex);
@@ -94,24 +89,21 @@ export const Tabs = ({
// when filename changes, reset error
useEffect(() => {
setTabnameError(null);
}, [tabname, setTabnameError]);
setNewtabError(null);
}, [tabname, setNewtabError]);
const validateTabname = useCallback(
(tabname: string): { error?: string, result?: string } => {
(tabname: string): { error: string | null } => {
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!" };
}
@@ -124,7 +116,7 @@ export const Tabs = ({
) {
return { error: headerExtraValidation.error };
}
return { result: tabname };
return { error: null };
},
[
allowedExtensions,
@@ -144,33 +136,16 @@ export const Tabs = ({
[onChangeActive]
);
const handleRenameTab = useCallback(() => {
if (renamingTab === null) return;
const res = validateTabname(tabname);
if (res.error) {
setTabnameError(`Error: ${res.error}`);
return;
}
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}`);
const chk = validateTabname(tabname);
if (chk.error) {
setNewtabError(`Error: ${chk.error}`);
return;
}
const { result: _tabname = tabname } = res
let _tabname = tabname;
if (defaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
setIsNewtabDialogOpen(false);
setTabname("");
@@ -178,7 +153,14 @@ export const Tabs = ({
onCreateNewTab?.(_tabname);
handleActiveChange(tabs.length, _tabname);
}, [validateTabname, tabname, onCreateNewTab, handleActiveChange, tabs.length]);
}, [
tabname,
defaultExtension,
validateTabname,
onCreateNewTab,
handleActiveChange,
tabs.length,
]);
const handleCloseTab = useCallback(
(idx: number) => {
@@ -193,21 +175,6 @@ 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 && (
@@ -222,14 +189,7 @@ export const Tabs = ({
}}
>
{tabs.map((tab, idx) => (
<ContextMenu
key={tab.header}
options={
[closeOption(idx), renameOption(idx)].filter(
Boolean
) as ContentMenuOption[]
}
>
<ContextMenu key={tab.header}>
<Button
role="tab"
tabIndex={idx}
@@ -301,7 +261,7 @@ export const Tabs = ({
}
}}
/>
<ErrorText>{tabnameError}</ErrorText>
<ErrorText>{newtabError}</ErrorText>
</DialogDescription>
<Flex
@@ -326,51 +286,6 @@ 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

View File

@@ -5,7 +5,7 @@ import state from "../../state";
import {
defaultTransactionType,
getTxFields,
modifyTxState,
modifyTransaction,
prepareState,
prepareTransaction,
SelectOption,
@@ -42,7 +42,7 @@ const Transaction: FC<TransactionProps> = ({
const setState = useCallback(
(pTx?: Partial<TransactionState>) => {
return modifyTxState(header, pTx);
return modifyTransaction(header, pTx);
},
[header]
);
@@ -164,7 +164,7 @@ const Transaction: FC<TransactionProps> = ({
}
nwState.txFields = fields;
const state = modifyTxState(header, nwState, { replaceState: true });
const state = modifyTransaction(header, nwState, { replaceState: true });
const editorValue = getJsonString(state);
return setState({ editorValue });
},

View File

@@ -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, { renameTxState } from "../../state";
import state from "../../state";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
import { transactionsState, modifyTxState } from "../../state";
import { transactionsState, modifyTransaction } 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,12 +96,9 @@ const Test = () => {
keepAllAlive
defaultExtension="json"
allowedExtensions={["json"]}
onCreateNewTab={header => modifyTxState(header, {})}
onRenameTab={(idx, nwName, oldName = "") =>
renameTxState(oldName, nwName)
}
onCreateNewTab={header => modifyTransaction(header, {})}
onCloseTab={(idx, header) =>
header && modifyTxState(header, undefined)
header && modifyTransaction(header, undefined)
}
>
{transactions.map(({ header, state }) => (

View File

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

View File

@@ -14,11 +14,4 @@ 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
};

View File

@@ -48,21 +48,13 @@ 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 modifyTxState = (
export const modifyTransaction = (
header: string,
partialTx?: Partial<TransactionState>,
opts: { replaceState?: boolean } = {}