Compare commits

...

6 Commits

Author SHA1 Message Date
muzam1l
bf792f1495 Fix dependecy issues. 2022-07-20 14:55:59 +05:30
muzam1l
df3210a663 Add context menu component 2022-07-20 14:12:48 +05:30
muzam1l
bad7730c32 Fix extension requirement error. 2022-07-20 14:05:02 +05:30
muzam1l
adb6a78549 update active compiled file based on active editor file. 2022-07-20 14:05:02 +05:30
muzam1l
8cc27f20c3 Migrate File navigation to tabs component. 2022-07-20 14:05:02 +05:30
muzamil
8e49a3f5f1 Merge pull request #253 from XRPLF/fix/json-tx
Fix json mode.
2022-07-20 13:59:42 +05:30
13 changed files with 527 additions and 361 deletions

View File

@@ -0,0 +1,152 @@
import { CaretRight, Check, Circle } from "phosphor-react";
import { FC, Fragment, ReactNode } from "react";
import { Flex, Text } from "../";
import {
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuItemIndicator,
ContextMenuLabel,
ContextMenuRadioGroup,
ContextMenuRadioItem,
ContextMenuRoot,
ContextMenuSeparator,
ContextMenuTrigger,
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[];
};
export type SeparatorOption = { type: "separator" };
export type CheckboxOption = {
type: "checkbox";
label: ReactNode;
checked?: boolean;
onCheckedChange?: (isChecked: boolean) => any;
};
export type RadioOption<T extends string = string> = {
type: "radio";
label: ReactNode;
onValueChange?: (value: string) => any;
value: T;
options?: { value: T; label?: ReactNode }[];
};
type WithCommons = { key: string; disabled?: boolean };
type Option = (TextOption | SeparatorOption | CheckboxOption | RadioOption) &
WithCommons;
interface IContextMenu {
options?: Option[];
isNested?: boolean;
}
export const ContextMenu: FC<IContextMenu> = ({
children,
options,
isNested,
}) => {
return (
<ContextMenuRoot>
{isNested ? (
<ContextMenuTriggerItem>{children}</ContextMenuTriggerItem>
) : (
<ContextMenuTrigger>{children}</ContextMenuTrigger>
)}
{options && (
<ContextMenuContent sideOffset={isNested ? 2 : 5}>
{options.map(({ key, ...option }) => {
if (option.type === "text") {
const { children, label } = option;
if (children)
return (
<ContextMenu isNested key={key} options={children}>
<Flex fluid row justify="space-between" align="center">
<Text>{label}</Text>
<CaretRight />
</Flex>
</ContextMenu>
);
return <ContextMenuItem key={key}>{label}</ContextMenuItem>;
}
if (option.type === "checkbox") {
const { label, checked, onCheckedChange } = option;
return (
<ContextMenuCheckboxItem
key={key}
checked={checked}
onCheckedChange={onCheckedChange}
>
<Flex row align="center">
<ContextMenuItemIndicator>
<Check />
</ContextMenuItemIndicator>
<Text css={{ ml: checked ? "$4" : undefined }}>
{label}
</Text>
</Flex>
</ContextMenuCheckboxItem>
);
}
if (option.type === "radio") {
const { label, options, onValueChange, value } = option;
return (
<Fragment key={key}>
<ContextMenuLabel>{label}</ContextMenuLabel>
<ContextMenuRadioGroup
value={value}
onValueChange={onValueChange}
>
{options?.map(({ value: v, label }) => {
return (
<ContextMenuRadioItem key={v} value={v}>
<ContextMenuItemIndicator>
<Circle weight="fill" />
</ContextMenuItemIndicator>
<Text css={{ ml: "$4" }}>{label}</Text>
</ContextMenuRadioItem>
);
})}
</ContextMenuRadioGroup>
</Fragment>
);
}
return <ContextMenuSeparator key={key} />;
})}
</ContextMenuContent>
)}
</ContextMenuRoot>
);
};
export default ContextMenu;

View File

@@ -0,0 +1,107 @@
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { styled } from "../../stitches.config";
import {
slideDownAndFade,
slideLeftAndFade,
slideRightAndFade,
slideUpAndFade,
} from "../../styles/keyframes";
const StyledContent = styled(ContextMenuPrimitive.Content, {
minWidth: 140,
backgroundColor: "$backgroundOverlay",
borderRadius: 6,
overflow: "hidden",
padding: "5px",
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
"@media (prefers-reduced-motion: no-preference)": {
animationDuration: "400ms",
animationTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
willChange: "transform, opacity",
'&[data-state="open"]': {
'&[data-side="top"]': { animationName: slideDownAndFade },
'&[data-side="right"]': { animationName: slideLeftAndFade },
'&[data-side="bottom"]': { animationName: slideUpAndFade },
'&[data-side="left"]': { animationName: slideRightAndFade },
},
},
".dark &": {
boxShadow:
"0px 10px 38px -10px rgba(22, 23, 24, 0.85), 0px 10px 20px -15px rgba(22, 23, 24, 0.6)",
},
});
const itemStyles = {
all: "unset",
fontSize: 13,
lineHeight: 1,
color: "$text",
borderRadius: 3,
display: "flex",
alignItems: "center",
height: 28,
padding: "0 7px",
position: "relative",
paddingLeft: 10,
userSelect: "none",
"&[data-disabled]": {
color: "$textMuted",
pointerEvents: "none",
},
"&:focus": {
backgroundColor: "$purple9",
color: "$white",
},
};
const StyledItem = styled(ContextMenuPrimitive.Item, { ...itemStyles });
const StyledCheckboxItem = styled(ContextMenuPrimitive.CheckboxItem, {
...itemStyles,
});
const StyledRadioItem = styled(ContextMenuPrimitive.RadioItem, {
...itemStyles,
});
const StyledTriggerItem = styled(ContextMenuPrimitive.TriggerItem, {
'&[data-state="open"]': {
backgroundColor: "$purple9",
color: "$purple9",
},
...itemStyles,
});
const StyledLabel = styled(ContextMenuPrimitive.Label, {
paddingLeft: 10,
fontSize: 12,
lineHeight: "25px",
color: "$text",
});
const StyledSeparator = styled(ContextMenuPrimitive.Separator, {
height: 1,
backgroundColor: "$backgroundAlt",
margin: 5,
});
const StyledItemIndicator = styled(ContextMenuPrimitive.ItemIndicator, {
position: "absolute",
left: 0,
width: 25,
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
export const ContextMenuRoot = ContextMenuPrimitive.Root;
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
export const ContextMenuContent = StyledContent;
export const ContextMenuItem = StyledItem;
export const ContextMenuCheckboxItem = StyledCheckboxItem;
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
export const ContextMenuRadioItem = StyledRadioItem;
export const ContextMenuItemIndicator = StyledItemIndicator;
export const ContextMenuTriggerItem = StyledTriggerItem;
export const ContextMenuLabel = StyledLabel;
export const ContextMenuSeparator = StyledSeparator;

View File

@@ -13,7 +13,7 @@ import state from "../state";
import wat from "../utils/wat-highlight"; import wat from "../utils/wat-highlight";
import EditorNavigation from "./EditorNavigation"; import EditorNavigation from "./EditorNavigation";
import { Button, Text, Link, Flex } from "."; import { Button, Text, Link, Flex, Tabs, Tab } from ".";
import Monaco from "./Monaco"; import Monaco from "./Monaco";
const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024]; const FILESIZE_BREAKPOINTS: [number, number] = [2 * 1024, 5 * 1024];
@@ -25,9 +25,20 @@ const DeployEditor = () => {
const [showContent, setShowContent] = useState(false); const [showContent, setShowContent] = useState(false);
const activeFile = snap.files[snap.active]?.compiledContent const compiledFiles = snap.files.filter(file => file.compiledContent);
? snap.files[snap.active] const activeFile = compiledFiles[snap.activeWat];
: snap.files.filter(file => file.compiledContent)[0];
const renderNav = () => (
<Tabs
activeIndex={snap.activeWat}
onChangeActive={idx => (state.activeWat = idx)}
>
{compiledFiles.map((file, index) => {
return <Tab key={file.name} header={`${file.name}.wat`} />;
})}
</Tabs>
);
const compiledSize = activeFile?.compiledContent?.byteLength || 0; const compiledSize = activeFile?.compiledContent?.byteLength || 0;
const color = const color =
compiledSize > FILESIZE_BREAKPOINTS[1] compiledSize > FILESIZE_BREAKPOINTS[1]
@@ -38,7 +49,7 @@ const DeployEditor = () => {
const isContentChanged = const isContentChanged =
activeFile && activeFile.compiledValueSnapshot !== activeFile.content; activeFile && activeFile.compiledValueSnapshot !== activeFile.content;
// const hasDeployErros = activeFile && activeFile.containsErrors; // const hasDeployErrors = activeFile && activeFile.containsErrors;
const CompiledStatView = activeFile && ( const CompiledStatView = activeFile && (
<Flex <Flex
@@ -99,6 +110,7 @@ const DeployEditor = () => {
</NextLink> </NextLink>
</Text> </Text>
); );
const isContent = const isContent =
snap.files?.filter(file => file.compiledWatContent).length > 0 && snap.files?.filter(file => file.compiledWatContent).length > 0 &&
router.isReady; router.isReady;
@@ -113,7 +125,7 @@ const DeployEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation showWat /> <EditorNavigation renderNav={renderNav} />
<Container <Container
css={{ css={{
display: "flex", display: "flex",

View File

@@ -1,27 +1,7 @@
import { keyframes } from "@stitches/react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { slideDownAndFade, slideLeftAndFade, slideRightAndFade, slideUpAndFade } from '../styles/keyframes';
const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
const StyledContent = styled(DropdownMenuPrimitive.Content, { const StyledContent = styled(DropdownMenuPrimitive.Content, {
minWidth: 220, minWidth: 220,

View File

@@ -1,6 +1,10 @@
import React, { useState, useEffect, useCallback, useRef } from "react"; import React, {
useState,
useEffect,
useRef,
ReactNode,
} from "react";
import { import {
Plus,
Share, Share,
DownloadSimple, DownloadSimple,
Gear, Gear,
@@ -28,7 +32,6 @@ import { useSnapshot } from "valtio";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import {
createNewFile,
syncToGist, syncToGist,
updateEditorSettings, updateEditorSettings,
downloadAsZip, downloadAsZip,
@@ -48,36 +51,23 @@ import {
import Flex from "./Flex"; import Flex from "./Flex";
import Stack from "./Stack"; import Stack from "./Stack";
import { Input, Label } from "./Input"; import { Input, Label } from "./Input";
import Text from "./Text";
import Tooltip from "./Tooltip"; import Tooltip from "./Tooltip";
import { styled } from "../stitches.config";
import { showAlert } from "../state/actions/showAlert"; import { showAlert } from "../state/actions/showAlert";
const ErrorText = styled(Text, {
color: "$error",
mt: "$1",
display: "block",
});
const EditorNavigation = ({ showWat }: { showWat?: boolean }) => { const EditorNavigation = ({ renderNav }: { renderNav?: () => ReactNode }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const [editorSettingsOpen, setEditorSettingsOpen] = useState(false); const [editorSettingsOpen, setEditorSettingsOpen] = useState(false);
const [isNewfileDialogOpen, setIsNewfileDialogOpen] = useState(false);
const [newfileError, setNewfileError] = useState<string | null>(null);
const [filename, setFilename] = useState("");
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const [popup, setPopUp] = useState(false); const [popup, setPopUp] = useState(false);
const [editorSettings, setEditorSettings] = useState(snap.editorSettings); const [editorSettings, setEditorSettings] = useState(snap.editorSettings);
useEffect(() => { useEffect(() => {
if (session && session.user && popup) { if (session && session.user && popup) {
setPopUp(false); setPopUp(false);
} }
}, [session, popup]); }, [session, popup]);
// when filename changes, reset error
useEffect(() => {
setNewfileError(null);
}, [filename, setNewfileError]);
const showNewGistAlert = () => { const showNewGistAlert = () => {
showAlert("Are you sure?", { showAlert("Are you sure?", {
@@ -95,46 +85,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
}); });
}; };
const validateFilename = useCallback(
(filename: string): { error: string | null } => {
// check if filename already exists
if (!filename) {
return { error: "You need to add filename" };
}
if (snap.files.find((file) => file.name === filename)) {
return { error: "Filename already exists." };
}
if (!filename.includes(".") || filename[filename.length - 1] === ".") {
return { error: "Filename should include file extension" };
}
// check for illegal characters
const ALPHA_NUMERICAL_REGEX = /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g;
if (!filename.match(ALPHA_NUMERICAL_REGEX)) {
return {
error: `Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-" and it needs to have file extension (e.g. ".c")`,
};
}
return { error: null };
},
[snap.files]
);
const handleConfirm = useCallback(() => {
// add default extension in case omitted
const chk = validateFilename(filename);
if (chk && chk.error) {
setNewfileError(`Error: ${chk.error}`);
return;
}
setIsNewfileDialogOpen(false);
createNewFile(filename);
setFilename("");
}, [filename, setIsNewfileDialogOpen, setFilename, validateFilename]);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const files = snap.files;
return ( return (
<Flex css={{ flexShrink: 0, gap: "$0" }}> <Flex css={{ flexShrink: 0, gap: "$0" }}>
<Flex <Flex
@@ -174,131 +126,14 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
scrollbarWidth: "thin", scrollbarWidth: "thin",
}, },
}} }}
onWheelCapture={(e) => { onWheelCapture={e => {
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollLeft += e.deltaY; scrollRef.current.scrollLeft += e.deltaY;
} }
}} }}
> >
<Container css={{ flex: 1 }} ref={containerRef}> <Container css={{ flex: 1 }} ref={containerRef}>
<Stack {renderNav?.()}
css={{
gap: "$3",
flex: 1,
flexWrap: "nowrap",
marginBottom: "-1px",
}}
>
{files &&
files.length > 0 &&
files.map((file, index) => {
if (!file.compiledContent && showWat) {
return null;
}
return (
<Button
size="sm"
outline={
showWat ? snap.activeWat !== index : snap.active !== index
}
onClick={() => (state.active = index)}
key={file.name + index}
css={{
"&:hover": {
span: {
visibility: "visible",
},
},
}}
>
{file.name}
{showWat && ".wat"}
{!showWat && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
// Remove file from state
state.files.splice(index, 1);
// Change active file state
// If deleted file is behind active tab
// we keep the current state otherwise
// select previous file on the list
state.active =
index > snap.active ? snap.active : snap.active - 1;
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
);
})}
{!showWat && (
<Dialog
open={isNewfileDialogOpen}
onOpenChange={setIsNewfileDialogOpen}
>
<DialogTrigger asChild>
<Button
ghost
size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }}
>
<Plus size="16px" />{" "}
{snap.files.length === 0 && "Add new file"}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Create new file</DialogTitle>
<DialogDescription>
<Label>Filename</Label>
<Input
value={filename}
onChange={(e) => setFilename(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter") {
handleConfirm();
}
}}
/>
<ErrorText>{newfileError}</ErrorText>
</DialogDescription>
<Flex
css={{
marginTop: 25,
justifyContent: "flex-end",
gap: "$3",
}}
>
<DialogClose asChild>
<Button outline>Cancel</Button>
</DialogClose>
<Button variant="primary" onClick={handleConfirm}>
Create file
</Button>
</Flex>
<DialogClose asChild>
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
<X size="20px" />
</Box>
</DialogClose>
</DialogContent>
</Dialog>
)}
</Stack>
</Container> </Container>
</Flex> </Flex>
<Flex <Flex
@@ -542,8 +377,8 @@ const EditorNavigation = ({ showWat }: { showWat?: boolean }) => {
type="number" type="number"
min="1" min="1"
value={editorSettings.tabSize} value={editorSettings.tabSize}
onChange={(e) => onChange={e =>
setEditorSettings((curr) => ({ setEditorSettings(curr => ({
...curr, ...curr,
tabSize: Number(e.target.value), tabSize: Number(e.target.value),
})) }))

View File

@@ -7,7 +7,7 @@ import { useRouter } from "next/router";
import Box from "./Box"; import Box from "./Box";
import Container from "./Container"; import Container from "./Container";
import { saveFile } from "../state/actions"; import { createNewFile, saveFile } from "../state/actions";
import { apiHeaderFiles } from "../state/constants"; import { apiHeaderFiles } from "../state/constants";
import state from "../state"; import state from "../state";
@@ -20,7 +20,8 @@ import ReconnectingWebSocket from "reconnecting-websocket";
import docs from "../xrpl-hooks-docs/docs"; import docs from "../xrpl-hooks-docs/docs";
import Monaco from "./Monaco"; import Monaco from "./Monaco";
import { saveAllFiles } from '../state/actions/saveFile'; import { saveAllFiles } from "../state/actions/saveFile";
import { Tab, Tabs } from "./Tabs";
const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => { const validateWritability = (editor: monaco.editor.IStandaloneCodeEditor) => {
const currPath = editor.getModel()?.uri.path; const currPath = editor.getModel()?.uri.path;
@@ -119,6 +120,26 @@ const HooksEditor = () => {
}, []); }, []);
const file = snap.files[snap.active]; const file = snap.files[snap.active];
const renderNav = () => (
<Tabs
label="File"
activeIndex={snap.active}
onChangeActive={idx => (state.active = idx)}
extensionRequired
onCreateNewTab={createNewFile}
onCloseTab={idx => state.files.splice(idx, 1)}
headerExtraValidation={{
regex: /^[A-Za-z0-9_-]+[.][A-Za-z0-9]{1,4}$/g,
error:
'Filename can contain only characters from a-z, A-Z, 0-9, "_" and "-"',
}}
>
{snap.files.map((file, index) => {
return <Tab key={file.name} header={file.name} />;
})}
</Tabs>
);
return ( return (
<Box <Box
css={{ css={{
@@ -131,7 +152,7 @@ const HooksEditor = () => {
width: "100%", width: "100%",
}} }}
> >
<EditorNavigation /> <EditorNavigation renderNav={renderNav} />
{snap.files.length > 0 && router.isReady ? ( {snap.files.length > 0 && router.isReady ? (
<Monaco <Monaco
keepCurrentModel keepCurrentModel

View File

@@ -22,7 +22,7 @@ import {
import { TTS, tts } from "../utils/hookOnCalculator"; import { TTS, tts } from "../utils/hookOnCalculator";
import { deployHook } from "../state/actions"; import { deployHook } from "../state/actions";
import { useSnapshot } from "valtio"; import { useSnapshot } from "valtio";
import state, { SelectOption } from "../state"; import state, { IFile, SelectOption } from "../state";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook"; import { prepareDeployHookTx, sha256 } from "../state/actions/deployHook";
import estimateFee from "../utils/estimateFee"; import estimateFee from "../utils/estimateFee";
@@ -56,9 +56,9 @@ export type SetHookData = {
export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo( export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
({ accountAddress }) => { ({ accountAddress }) => {
const snap = useSnapshot(state); const snap = useSnapshot(state);
const activeFile = snap.files[snap.active]?.compiledContent const compiledFiles = snap.files.filter(file => file.compiledContent);
? snap.files[snap.active] const activeFile = compiledFiles[snap.activeWat] as IFile | undefined;
: snap.files.filter(file => file.compiledContent)[0];
const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false); const [isSetHookDialogOpen, setIsSetHookDialogOpen] = useState(false);
const accountOptions: SelectOption[] = snap.accounts.map(acc => ({ const accountOptions: SelectOption[] = snap.accounts.map(acc => ({
@@ -72,6 +72,15 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const account = snap.accounts.find( const account = snap.accounts.find(
acc => acc.address === selectedAccount?.value acc => acc.address === selectedAccount?.value
); );
const getHookNamespace = useCallback(
() =>
activeFile && snap.deployValues[activeFile.name]
? snap.deployValues[activeFile.name].HookNamespace
: activeFile?.name.split(".")[0] || "",
[activeFile, snap.deployValues]
);
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -81,13 +90,10 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
getValues, getValues,
formState: { errors }, formState: { errors },
} = useForm<SetHookData>({ } = useForm<SetHookData>({
defaultValues: snap.deployValues?.[activeFile?.name] defaultValues: (activeFile && snap.deployValues[activeFile.name]) || {
? snap.deployValues[activeFile?.name] HookNamespace: activeFile?.name.split(".")[0] || "",
: { Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
HookNamespace: },
snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "",
Invoke: transactionOptions.filter(to => to.label === "ttPAYMENT"),
},
}); });
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control, control,
@@ -97,20 +103,15 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
const [estimateLoading, setEstimateLoading] = useState(false); const [estimateLoading, setEstimateLoading] = useState(false);
const watchedFee = watch("Fee"); const watchedFee = watch("Fee");
// Update value if activeWat changes // Update value if activeFile changes
useEffect(() => { useEffect(() => {
const defaultValue = snap.deployValues?.[activeFile?.name] if (!activeFile) return;
? snap.deployValues?.[activeFile?.name].HookNamespace const defaultValue = getHookNamespace();
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || "";
setValue("HookNamespace", defaultValue); setValue("HookNamespace", defaultValue);
setFormInitialized(true); setFormInitialized(true);
}, [ }, [setValue, activeFile, snap.deployValues, getHookNamespace]);
snap.activeWat,
snap.files,
setValue,
activeFile?.name,
snap.deployValues,
]);
useEffect(() => { useEffect(() => {
if ( if (
watchedFee && watchedFee &&
@@ -128,21 +129,19 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
// name: "HookGrants", // unique name for your Field Array // name: "HookGrants", // unique name for your Field Array
// }); // });
const [hashedNamespace, setHashedNamespace] = useState(""); const [hashedNamespace, setHashedNamespace] = useState("");
const namespace = watch(
"HookNamespace", const namespace = watch("HookNamespace", getHookNamespace());
snap.deployValues?.[activeFile?.name]
? snap.deployValues?.[activeFile?.name].HookNamespace
: snap.files?.[snap.activeWat]?.name?.split(".")?.[0] || ""
);
const calculateHashedValue = useCallback(async () => { const calculateHashedValue = useCallback(async () => {
const hashedVal = await sha256(namespace); const hashedVal = await sha256(namespace);
setHashedNamespace(hashedVal.toUpperCase()); setHashedNamespace(hashedVal.toUpperCase());
}, [namespace]); }, [namespace]);
useEffect(() => { useEffect(() => {
calculateHashedValue(); calculateHashedValue();
}, [namespace, calculateHashedValue]); }, [namespace, calculateHashedValue]);
// Calcucate initial fee estimate when modal opens // Calculate initial fee estimate when modal opens
useEffect(() => { useEffect(() => {
if (formInitialized && account) { if (formInitialized && account) {
(async () => { (async () => {
@@ -161,18 +160,15 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
}, [formInitialized]); }, [formInitialized]);
const tooLargeFile = () => { const tooLargeFile = () => {
const activeFile = snap.files[snap.active].compiledContent
? snap.files[snap.active]
: snap.files.filter(file => file.compiledContent)[0];
return Boolean( return Boolean(
activeFile?.compiledContent?.byteLength && activeFile?.compiledContent?.byteLength &&
activeFile?.compiledContent?.byteLength >= 64000 activeFile?.compiledContent?.byteLength >= 64000
); );
}; };
const onSubmit: SubmitHandler<SetHookData> = async (data) => { const onSubmit: SubmitHandler<SetHookData> = async data => {
const currAccount = state.accounts.find( const currAccount = state.accounts.find(
(acc) => acc.address === account?.address acc => acc.address === account?.address
); );
if (!account) return; if (!account) return;
if (currAccount) currAccount.isLoading = true; if (currAccount) currAccount.isLoading = true;
@@ -194,10 +190,7 @@ export const SetHookDialog: React.FC<{ accountAddress: string }> = React.memo(
uppercase uppercase
variant={"secondary"} variant={"secondary"}
disabled={ disabled={
!account || !account || account.isLoading || !activeFile || tooLargeFile()
account.isLoading ||
!snap.files.filter(file => file.compiledWatContent).length ||
tooLargeFile()
} }
> >
Set Hook Set Hook

View File

@@ -17,6 +17,8 @@ import {
} from "./Dialog"; } from "./Dialog";
import { Plus, X } from "phosphor-react"; import { Plus, X } from "phosphor-react";
import { styled } from "../stitches.config"; import { styled } from "../stitches.config";
import { capitalize } from "../utils/helpers";
import ContextMenu from "./ContextMenu";
const ErrorText = styled(Text, { const ErrorText = styled(Text, {
color: "$error", color: "$error",
@@ -25,11 +27,11 @@ const ErrorText = styled(Text, {
}); });
interface TabProps { interface TabProps {
header?: string; header: string;
children: ReactNode; children?: ReactNode;
} }
// TODO customise messages shown // TODO customize messages shown
interface Props { interface Props {
label?: string; label?: string;
activeIndex?: number; activeIndex?: number;
@@ -38,8 +40,12 @@ interface Props {
children: ReactElement<TabProps>[]; children: ReactElement<TabProps>[];
keepAllAlive?: boolean; keepAllAlive?: boolean;
defaultExtension?: string; defaultExtension?: string;
appendDefaultExtension?: boolean; extensionRequired?: boolean;
allowedExtensions?: string[]; allowedExtensions?: string[];
headerExtraValidation?: {
regex: string | RegExp;
error: string;
};
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; onChangeActive?: (index: number, header?: string) => any;
@@ -57,8 +63,9 @@ export const Tabs = ({
onCreateNewTab, onCreateNewTab,
onCloseTab, onCloseTab,
onChangeActive, onChangeActive,
headerExtraValidation,
extensionRequired,
defaultExtension = "", defaultExtension = "",
appendDefaultExtension = false,
allowedExtensions, allowedExtensions,
}: Props) => { }: Props) => {
const [active, setActive] = useState(activeIndex || 0); const [active, setActive] = useState(activeIndex || 0);
@@ -87,16 +94,38 @@ 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 (!tabname) {
return { error: "Name already exists." }; return { error: `Please enter ${label.toLocaleLowerCase()} name.` };
}
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!" };
} }
const ext = tabname.split(".").pop() || "";
if (allowedExtensions && !allowedExtensions.includes(ext)) { if (allowedExtensions && !allowedExtensions.includes(ext)) {
return { error: "This file extension is not allowed!" }; return { error: "This file extension is not allowed!" };
} }
if (
headerExtraValidation &&
!tabname.match(headerExtraValidation.regex)
) {
return { error: headerExtraValidation.error };
}
return { error: null }; return { error: null };
}, },
[allowedExtensions, tabs] [
allowedExtensions,
defaultExtension,
extensionRequired,
headerExtraValidation,
label,
tabs,
]
); );
const handleActiveChange = useCallback( const handleActiveChange = useCallback(
@@ -108,31 +137,25 @@ export const Tabs = ({
); );
const handleCreateTab = useCallback(() => { const handleCreateTab = useCallback(() => {
// add default extension in case omitted const chk = validateTabname(tabname);
let _tabname = tabname.includes(".")
? tabname
: `${tabname}.${defaultExtension}`;
if (appendDefaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
const chk = validateTabname(_tabname);
if (chk.error) { if (chk.error) {
setNewtabError(`Error: ${chk.error}`); setNewtabError(`Error: ${chk.error}`);
return; return;
} }
let _tabname = tabname;
if (defaultExtension && !_tabname.endsWith(defaultExtension)) {
_tabname = `${_tabname}.${defaultExtension}`;
}
setIsNewtabDialogOpen(false); setIsNewtabDialogOpen(false);
setTabname(""); setTabname("");
onCreateNewTab?.(_tabname); onCreateNewTab?.(_tabname);
// switch to new tab?
handleActiveChange(tabs.length, _tabname); handleActiveChange(tabs.length, _tabname);
}, [ }, [
tabname, tabname,
defaultExtension, defaultExtension,
appendDefaultExtension,
validateTabname, validateTabname,
onCreateNewTab, onCreateNewTab,
handleActiveChange, handleActiveChange,
@@ -146,8 +169,10 @@ export const Tabs = ({
} }
onCloseTab?.(idx, tabs[idx].header); onCloseTab?.(idx, tabs[idx].header);
handleActiveChange(idx, tabs[idx].header);
}, },
[active, onCloseTab, tabs] [active, handleActiveChange, onCloseTab, tabs]
); );
return ( return (
@@ -164,46 +189,47 @@ export const Tabs = ({
}} }}
> >
{tabs.map((tab, idx) => ( {tabs.map((tab, idx) => (
<Button <ContextMenu key={tab.header}>
key={tab.header} <Button
role="tab" role="tab"
tabIndex={idx} tabIndex={idx}
onClick={() => handleActiveChange(idx, tab.header)} onClick={() => handleActiveChange(idx, tab.header)}
onKeyPress={() => handleActiveChange(idx, tab.header)} onKeyPress={() => handleActiveChange(idx, tab.header)}
outline={active !== idx} outline={active !== idx}
size="sm" size="sm"
css={{ css={{
"&:hover": { "&:hover": {
span: { span: {
visibility: "visible", visibility: "visible",
},
},
}}
>
{tab.header || idx}
{onCloseTab && (
<Box
as="span"
css={{
display: "flex",
p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
}, },
}} },
onClick={(ev: React.MouseEvent<HTMLElement>) => { }}
ev.stopPropagation(); >
handleCloseTab(idx); {tab.header || idx}
}} {onCloseTab && (
> <Box
<X size="9px" weight="bold" /> as="span"
</Box> css={{
)} display: "flex",
</Button> p: "2px",
borderRadius: "$full",
mr: "-4px",
"&:hover": {
// boxSizing: "0px 0px 1px",
backgroundColor: "$mauve2",
color: "$mauve12",
},
}}
onClick={(ev: React.MouseEvent<HTMLElement>) => {
ev.stopPropagation();
handleCloseTab(idx);
}}
>
<X size="9px" weight="bold" />
</Box>
)}
</Button>
</ContextMenu>
))} ))}
{onCreateNewTab && ( {onCreateNewTab && (
<Dialog <Dialog
@@ -216,11 +242,14 @@ export const Tabs = ({
size="sm" size="sm"
css={{ alignItems: "center", px: "$2", mr: "$3" }} css={{ alignItems: "center", px: "$2", mr: "$3" }}
> >
<Plus size="16px" /> {tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`} <Plus size="16px" />{" "}
{tabs.length === 0 && `Add new ${label.toLocaleLowerCase()}`}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<DialogTitle>Create new {label.toLocaleLowerCase()}</DialogTitle> <DialogTitle>
Create new {label.toLocaleLowerCase()}
</DialogTitle>
<DialogDescription> <DialogDescription>
<Label>{label} name</Label> <Label>{label} name</Label>
<Input <Input
@@ -259,29 +288,32 @@ export const Tabs = ({
)} )}
</Stack> </Stack>
)} )}
{keepAllAlive ? ( {keepAllAlive
tabs.map((tab, idx) => { ? tabs.map((tab, idx) => {
// TODO Maybe rule out fragments as children // TODO Maybe rule out fragments as children
if (!isValidElement(tab.children)) { if (!isValidElement(tab.children)) {
if (active !== idx) return null; if (active !== idx) return null;
return tab.children; return tab.children;
} }
let key = tab.children.key || tab.header || idx; let key = tab.children.key || tab.header || idx;
let { children } = tab; let { children } = tab;
let { style, ...props } = children.props; let { style, ...props } = children.props;
return ( return (
<children.type <children.type
key={key} key={key}
{...props} {...props}
style={{ ...style, display: active !== idx ? "none" : undefined }} style={{
/> ...style,
); display: active !== idx ? "none" : undefined,
}) }}
) : ( />
<Fragment key={tabs[active].header || active}> );
{tabs[active].children} })
</Fragment> : tabs[active] && (
)} <Fragment key={tabs[active].header || active}>
{tabs[active].children}
</Fragment>
)}
</> </>
); );
}; };

View File

@@ -16,8 +16,9 @@
"@octokit/core": "^3.5.1", "@octokit/core": "^3.5.1",
"@radix-ui/colors": "^0.1.7", "@radix-ui/colors": "^0.1.7",
"@radix-ui/react-alert-dialog": "^0.1.1", "@radix-ui/react-alert-dialog": "^0.1.1",
"@radix-ui/react-context-menu": "^0.1.6",
"@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.6",
"@radix-ui/react-id": "^0.1.1", "@radix-ui/react-id": "^0.1.1",
"@radix-ui/react-label": "^0.1.5", "@radix-ui/react-label": "^0.1.5",
"@radix-ui/react-popover": "^0.1.6", "@radix-ui/react-popover": "^0.1.6",
@@ -35,7 +36,7 @@
"lodash.xor": "^4.5.0", "lodash.xor": "^4.5.0",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"next": "^12.0.4", "next": "^12.0.4",
"next-auth": "^4.0.0-beta.5", "next-auth": "^4.10.1",
"next-plausible": "^3.2.0", "next-plausible": "^3.2.0",
"next-themes": "^0.1.1", "next-themes": "^0.1.1",
"normalize-url": "^7.0.2", "normalize-url": "^7.0.2",
@@ -75,5 +76,8 @@
"eslint-config-next": "11.1.2", "eslint-config-next": "11.1.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "4.4.4" "typescript": "4.4.4"
},
"resolutions": {
"ripple-binary-codec": "=1.4.2"
} }
} }

View File

@@ -1,6 +1,6 @@
import type monaco from "monaco-editor"; import type monaco from "monaco-editor";
import { proxy, ref, subscribe } from "valtio"; import { proxy, ref, subscribe } from "valtio";
import { devtools } from 'valtio/utils'; import { devtools, subscribeKey } from 'valtio/utils';
import { XrplClient } from "xrpl-client"; import { XrplClient } from "xrpl-client";
import { SplitSize } from "./actions/persistSplits"; import { SplitSize } from "./actions/persistSplits";
@@ -168,16 +168,23 @@ if (process.env.NODE_ENV !== "production") {
} }
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
subscribe(state, () => { subscribe(state.accounts, () => {
const { accounts, active } = state; const { accounts } = state;
const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false })) const accountsNoLoading = accounts.map(acc => ({ ...acc, isLoading: false }))
localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading)); localStorage.setItem("hooksIdeAccounts", JSON.stringify(accountsNoLoading));
if (!state.files[active]?.compiledWatContent) {
state.activeWat = 0;
} else {
state.activeWat = active;
}
}); });
const updateActiveWat = () => {
const filename = state.files[state.active]?.name
const compiledFiles = state.files.filter(
file => file.compiledContent)
const idx = compiledFiles.findIndex(file => file.name === filename)
if (idx !== -1) state.activeWat = idx
}
subscribeKey(state, 'active', updateActiveWat)
subscribeKey(state, 'files', updateActiveWat)
} }
export default state export default state

View File

@@ -53,6 +53,7 @@ export const {
accent: "#9D2DFF", accent: "#9D2DFF",
background: "$gray1", background: "$gray1",
backgroundAlt: "$gray4", backgroundAlt: "$gray4",
backgroundOverlay: "$mauve2",
text: "$gray12", text: "$gray12",
textMuted: "$gray10", textMuted: "$gray10",
primary: "$plum", primary: "$plum",
@@ -365,6 +366,7 @@ export const darkTheme = createTheme("dark", {
...greenDark, ...greenDark,
...redDark, ...redDark,
deep: "rgb(10, 10, 10)", deep: "rgb(10, 10, 10)",
backgroundOverlay: "$mauve5"
// backgroundA: transparentize(0.1, grayDark.gray1), // backgroundA: transparentize(0.1, grayDark.gray1),
}, },
}); });

21
styles/keyframes.ts Normal file
View File

@@ -0,0 +1,21 @@
import { keyframes } from '../stitches.config';
export const slideUpAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
export const slideRightAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(-2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});
export const slideDownAndFade = keyframes({
"0%": { opacity: 0, transform: "translateY(-2px)" },
"100%": { opacity: 1, transform: "translateY(0)" },
});
export const slideLeftAndFade = keyframes({
"0%": { opacity: 0, transform: "translateX(2px)" },
"100%": { opacity: 1, transform: "translateX(0)" },
});

View File

@@ -594,6 +594,18 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-context-menu@^0.1.6":
version "0.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-0.1.6.tgz#0c75f2faffec6c8697247a4b685a432b3c4d07f0"
integrity sha512-0qa6ABaeqD+WYI+8iT0jH0QLLcV8Kv0xI+mZL4FFnG4ec9H0v+yngb5cfBBfs9e/KM8mDzFFpaeegqsQlLNqyQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.1.0"
"@radix-ui/react-context" "0.1.1"
"@radix-ui/react-menu" "0.1.6"
"@radix-ui/react-primitive" "0.1.4"
"@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-context@0.1.1": "@radix-ui/react-context@0.1.1":
version "0.1.1" version "0.1.1"
resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz" resolved "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-0.1.1.tgz"
@@ -635,9 +647,9 @@
"@radix-ui/react-use-callback-ref" "0.1.0" "@radix-ui/react-use-callback-ref" "0.1.0"
"@radix-ui/react-use-escape-keydown" "0.1.0" "@radix-ui/react-use-escape-keydown" "0.1.0"
"@radix-ui/react-dropdown-menu@^0.1.1": "@radix-ui/react-dropdown-menu@^0.1.6":
version "0.1.6" version "0.1.6"
resolved "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.1.6.tgz#3203229788cd57e552c9f19dcc7008e2b545919c"
integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg== integrity sha512-RZhtzjWwJ4ZBN7D8ek4Zn+ilHzYuYta9yIxFnbC0pfqMnSi67IQNONo1tuuNqtFh9SRHacPKc65zo+kBBlxtdg==
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
@@ -2941,10 +2953,10 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next-auth@^4.0.0-beta.5: next-auth@^4.10.1:
version "4.2.1" version "4.10.1"
resolved "https://registry.npmjs.org/next-auth/-/next-auth-4.2.1.tgz" resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.10.1.tgz#33b29265d12287bb2f6d267c8d415a407c27f0e9"
integrity sha512-XDtt7nqevkNf4EJ2zKAKkI+MFsURf11kx11vPwxrBYA1MHeqWwaWbGOUOI2ekNTvfAg4nTEJJUH3LV2cLrH3Tg== integrity sha512-F00vtwBdyMIIJ8IORHOAOHjVGTDEhhm9+HpB2BQ8r40WtGxqToWWLN7Z+2ZW/z2RFlo3zhcuAtUCPUzVJxtZwQ==
dependencies: dependencies:
"@babel/runtime" "^7.16.3" "@babel/runtime" "^7.16.3"
"@panva/hkdf" "^1.0.1" "@panva/hkdf" "^1.0.1"
@@ -3638,19 +3650,7 @@ ripple-address-codec@^4.2.4:
base-x "3.0.9" base-x "3.0.9"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-binary-codec@^1.1.3: ripple-binary-codec@=1.4.2, ripple-binary-codec@^0.2.4, ripple-binary-codec@^1.1.3, ripple-binary-codec@^1.4.2:
version "1.3.2"
resolved "https://registry.npmjs.org/ripple-binary-codec/-/ripple-binary-codec-1.3.2.tgz"
integrity sha512-8VG1vfb3EM1J7ZdPXo9E57Zv2hF4cxT64gP6rGSQzODVgMjiBCWozhN3729qNTGtHItz0e82Oix8v95vWYBQ3A==
dependencies:
assert "^2.0.0"
big-integer "^1.6.48"
buffer "5.6.0"
create-hash "^1.2.0"
decimal.js "^10.2.0"
ripple-address-codec "^4.2.3"
ripple-binary-codec@^1.4.2:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3" resolved "https://registry.yarnpkg.com/ripple-binary-codec/-/ripple-binary-codec-1.4.2.tgz#cdc35353e4bc7c3a704719247c82b4c4d0b57dd3"
integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w== integrity sha512-EDKIyZMa/6Ay/oNgCwjD9b9CJv0zmBreeHVQeG4BYwy+9GPnIQjNeT5e/aB6OjAnhcmpgbPeBmzwmNVwzxlt0w==
@@ -3685,7 +3685,7 @@ ripple-hashes@^0.3.4, ripple-hashes@latest:
bignumber.js "^4.1.0" bignumber.js "^4.1.0"
create-hash "^1.1.2" create-hash "^1.1.2"
ripple-address-codec "^3.0.4" ripple-address-codec "^3.0.4"
ripple-binary-codec "^1.4.2" ripple-binary-codec "^0.2.4"
ripple-keypairs@^1.0.3, ripple-keypairs@latest: ripple-keypairs@^1.0.3, ripple-keypairs@latest:
version "1.1.3" version "1.1.3"