Compare commits

...

12 Commits

Author SHA1 Message Date
Valtteri Karesto
89f8671217 Clear log should now work 2022-06-21 10:53:52 +03:00
Valtteri Karesto
fb5259221b Changed color of starting running 2022-06-21 09:43:29 +03:00
Valtteri Karesto
fd17f59616 Show LogBoxForScrips if js file active 2022-06-20 23:54:56 +03:00
Valtteri Karesto
91bbc7ea61 Catch template errors, add better labels, styling 2022-06-20 23:54:33 +03:00
Valtteri Karesto
783d832c6d Remove export for unused component 2022-06-20 23:53:32 +03:00
Valtteri Karesto
698ca376e7 Add showbuttons prop to LogBoxForScripts 2022-06-20 23:53:15 +03:00
Valtteri Karesto
bfd9e21ab8 Remove unused component 2022-06-20 23:52:45 +03:00
Valtteri Karesto
e46411f245 Rename template helpers 2022-06-20 14:53:30 +03:00
Valtteri Karesto
08447c6b29 Add support for select parameters 2022-06-20 14:16:16 +03:00
Valtteri Karesto
9216cc6bf7 When downloading zip, include wasm if it exists 2022-06-20 11:01:13 +03:00
Valtteri Karesto
5108b08e39 Do not show scripts panel if no supplementary scripts 2022-06-20 10:40:47 +03:00
Valtteri Karesto
6c46a4f809 Merge pull request #211 from XRPLF/feat/user-provided-scripts
Feat/user provided scripts
2022-06-20 10:16:06 +03:00
7 changed files with 252 additions and 166 deletions

View File

@@ -1,103 +0,0 @@
import React, { useRef, useLayoutEffect } from "react";
import { useSnapshot } from "valtio";
import { Play, Prohibit } from "phosphor-react";
import useStayScrolled from "react-stay-scrolled";
import Container from "./Container";
import Box from "./Box";
import LogText from "./LogText";
import { compileCode } from "../state/actions";
import state from "../state";
import Button from "./Button";
import Heading from "./Heading";
const Footer = () => {
const snap = useSnapshot(state);
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
useLayoutEffect(() => {
stayScrolled();
}, [snap.logs, stayScrolled]);
return (
<Box
as="footer"
css={{
display: "flex",
borderTop: "1px solid $mauve6",
background: "$mauve1",
position: "relative",
}}
>
<Container css={{ py: "$3", flexShrink: 1 }}>
<Heading
as="h3"
css={{ fontWeight: 300, m: 0, fontSize: "11px", color: "$mauve9" }}
>
DEVELOPMENT LOG
</Heading>
<Button
ghost
size="xs"
css={{
position: "absolute",
right: "$3",
top: "$2",
color: "$mauve10",
}}
onClick={() => {
state.logs = [];
}}
>
<Prohibit size="14px" />
</Button>
<Box
as="pre"
ref={logRef}
css={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "160px",
fontSize: "13px",
fontWeight: "$body",
fontFamily: "$monospace",
overflowY: "auto",
wordWrap: "break-word",
py: 3,
}}
>
{snap.logs?.map((log, index) => (
<Box as="span" key={log.type + index}>
<LogText capitalize variant={log.type}>
{log.type}:{" "}
</LogText>
<LogText>{log.message}</LogText>
</Box>
))}
</Box>
<Button
variant="primary"
uppercase
disabled={!snap.files.length}
isLoading={snap.compiling}
onClick={() => compileCode(snap.active)}
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
}}
>
<Play weight="bold" size="16px" />
Compile to Wasm
</Button>
</Container>
</Box>
);
};
export default Footer;

View File

@@ -25,6 +25,7 @@ interface ILogBox {
logs: ILog[];
renderNav?: () => ReactNode;
enhanced?: boolean;
showButtons?: boolean;
}
const LogBox: FC<ILogBox> = ({
@@ -34,6 +35,7 @@ const LogBox: FC<ILogBox> = ({
children,
renderNav,
enhanced,
showButtons = true,
}) => {
const logRef = useRef<HTMLPreElement>(null);
const { stayScrolled /*, scrollBottom*/ } = useStayScrolled(logRef);
@@ -86,13 +88,15 @@ const LogBox: FC<ILogBox> = ({
>
<FileJs size="15px" /> <Text css={{ lineHeight: 1 }}>{title}</Text>
</Heading>
<Flex css={{ gap: "$3" }}>
{snap.files
.filter((f) => f.name.endsWith(".js"))
.map((file) => (
<RunScript file={file} key={file.name} />
))}
</Flex>
{showButtons && (
<Flex css={{ gap: "$3" }}>
{snap.files
.filter((f) => f.name.endsWith(".js"))
.map((file) => (
<RunScript file={file} key={file.name} />
))}
</Flex>
)}
<Flex css={{ ml: "auto", gap: "$3", marginRight: "$3" }}>
{clearLog && (
<Button ghost size="xs" onClick={clearLog}>

View File

@@ -1,6 +1,6 @@
import Handlebars from "handlebars";
import * as Handlebars from "handlebars";
import { Play, X } from "phosphor-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import state, { IFile, ILog } from "../../state";
import Button from "../Button";
import Box from "../Box";
@@ -16,6 +16,15 @@ import {
} from "../Dialog";
import Flex from "../Flex";
import { useSnapshot } from "valtio";
import Select from "../Select";
import { saveFile } from "../../state/actions/saveFile";
Handlebars.registerHelper(
"customize_input",
function (/* dynamic arguments */) {
return new Handlebars.SafeString(arguments[0]);
}
);
const generateHtmlTemplate = (code: string) => {
return `
@@ -48,7 +57,7 @@ const generateHtmlTemplate = (code: string) => {
}
</script>
<script type="module">
${code}
${code}
</script>
</head>
<body>
@@ -56,22 +65,87 @@ const generateHtmlTemplate = (code: string) => {
</html>
`;
};
const RunScript: React.FC<{ file: IFile }> = ({ file }) => {
type Fields = Record<
string,
{
key: string;
value: string;
label?: string;
type?: string;
attach?: "account_secret" | "account_address" | string;
}
>;
const RunScript: React.FC<{ file: IFile }> = ({ file: { content, name } }) => {
const snap = useSnapshot(state);
const parsed = Handlebars.parse(file.content);
const names = parsed.body
.filter((i) => i.type === "MustacheStatement")
// @ts-expect-error
.map((block) => block?.path?.original);
const defaultState: Record<string, string> = {};
names.forEach((name) => (defaultState[name] = ""));
const [fields, setFields] = useState<Record<string, string>>(defaultState);
const [templateError, setTemplateError] = useState("");
const getFieldValues = useCallback(() => {
try {
const parsed = Handlebars.parse(content);
const names = parsed.body
.filter((i) => i.type === "MustacheStatement")
.map((block) => {
// @ts-expect-error
const type = block.hash?.pairs?.find((i) => i.key == "type");
// @ts-expect-error
const attach = block.hash?.pairs?.find((i) => i.key == "attach");
// @ts-expect-error
const label = block.hash?.pairs?.find((i) => i.key == "label");
const key =
// @ts-expect-error
block?.path?.original === "customize_input"
? // @ts-expect-error
block?.params?.[0].original
: // @ts-expect-error
block?.path?.original;
return {
key,
label: label?.value?.original || key,
attach: attach?.value?.original,
type: type?.value?.original,
value: "",
};
});
const defaultState: Fields = {};
if (names) {
names.forEach((field) => (defaultState[field.key] = field));
}
setTemplateError("");
return defaultState;
} catch (err) {
console.log(err);
setTemplateError("Could not parse template");
return undefined;
}
}, [content]);
// const defaultFieldValues = getFieldValues();
const [fields, setFields] = useState<Fields>({});
const [iFrameCode, setIframeCode] = useState("");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const runScript = () => {
const template = Handlebars.compile(file.content);
const code = template(fields);
setIframeCode(generateHtmlTemplate(code));
const fieldsToSend: Record<string, string> = {};
Object.entries(fields).map(([key, obj]) => {
fieldsToSend[key] = obj.value;
});
const template = Handlebars.compile(content, { strict: false });
try {
const code = template(fieldsToSend);
setIframeCode(generateHtmlTemplate(code));
state.scriptLogs = [
...snap.scriptLogs,
{ type: "success", message: "Started running..." },
];
} catch (err) {
state.scriptLogs = [
...snap.scriptLogs,
// @ts-expect-error
{ type: "error", message: err?.message || "Could not parse template" },
];
}
};
useEffect(() => {
@@ -88,6 +162,18 @@ const RunScript: React.FC<{ file: IFile }> = ({ file }) => {
return () => window.removeEventListener("message", handleEvent);
}, [snap.scriptLogs]);
useEffect(() => {
const newDefaultState = getFieldValues();
setFields(newDefaultState || {});
}, [content, setFields, getFieldValues]);
const options = snap.accounts?.map((acc) => ({
label: acc.name,
secret: acc.secret,
address: acc.address,
value: acc.address,
}));
return (
<>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
@@ -95,18 +181,28 @@ const RunScript: React.FC<{ file: IFile }> = ({ file }) => {
<Button
variant="primary"
onClick={() => {
saveFile(false);
setIframeCode("");
}}
>
{file.name} <Play weight="bold" size="16px" />
<Play weight="bold" size="16px" /> {name}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Run {file.name} script</DialogTitle>
<DialogTitle>Run {name} script</DialogTitle>
<DialogDescription>
You are about to run scripts provided by the developer of the hook,
make sure you know what you are doing.
<br />
{templateError && (
<Box
as="span"
css={{ display: "block", color: "$error", mt: "$3" }}
>
Error occured while parsing template, modify script and try
again!
</Box>
)}
<br />
{Object.keys(fields).length > 0
? `You also need to fill in following parameters to run the script`
@@ -115,15 +211,52 @@ const RunScript: React.FC<{ file: IFile }> = ({ file }) => {
<Stack css={{ width: "100%" }}>
{Object.keys(fields).map((key) => (
<Box key={key} css={{ width: "100%" }}>
<label>{key}</label>
<Input
type="text"
value={fields[key]}
css={{ mt: "$1" }}
onChange={(e) =>
setFields({ ...fields, [key]: e.target.value })
}
/>
<label>
{fields[key]?.label || key}{" "}
{fields[key].attach === "account_secret" &&
`(Script uses account secret)`}
</label>
{fields[key].attach === "account_secret" ||
fields[key].attach === "account_address" ? (
<Select
css={{ mt: "$1" }}
options={options}
onChange={(val: any) => {
setFields({
...fields,
[key]: {
...fields[key],
value:
fields[key].attach === "account_secret"
? val.secret
: val.address,
},
});
}}
value={options.find(
(opt) =>
opt.address === fields[key].value ||
opt.secret === fields[key].value
)}
/>
) : (
<Input
type={fields[key].type || "text"}
value={
typeof fields[key].value !== "string"
? // @ts-expect-error
fields[key].value.value
: fields[key].value
}
css={{ mt: "$1" }}
onChange={(e) => {
setFields({
...fields,
[key]: { ...fields[key], value: e.target.value },
});
}}
/>
)}
</Box>
))}
<Flex
@@ -135,10 +268,9 @@ const RunScript: React.FC<{ file: IFile }> = ({ file }) => {
<Button
variant="primary"
isDisabled={
Object.entries(fields).length > 0 &&
Object.entries(fields).every(
([key, value]: [string, string]) => !value
)
(Object.entries(fields).length > 0 &&
Object.entries(fields).some(([key, obj]) => !obj.value)) ||
Boolean(templateError)
}
onClick={() => {
state.scriptLogs = [];

View File

@@ -12,6 +12,5 @@ export { default as Box } from "./Box";
export { default as Button } from "./Button";
export { default as Pre } from "./Pre";
export { default as ButtonGroup } from "./ButtonGroup";
export { default as DeployFooter } from "./DeployFooter";
export * from "./Dialog";
export * from "./DropdownMenu";

View File

@@ -8,7 +8,9 @@ import { useSnapshot } from "valtio";
import { ButtonGroup, Flex } from "../../components";
import Box from "../../components/Box";
import Button from "../../components/Button";
import LogBoxForScripts from "../../components/LogBoxForScripts";
import Popover from "../../components/Popover";
import RunScript from "../../components/RunScript";
import state from "../../state";
import { compileCode } from "../../state/actions";
import { getSplit, saveSplit } from "../../state/actions/persistSplits";
@@ -196,20 +198,61 @@ const Home: NextPage = () => {
</Flex>
</Hotkeys>
)}
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Hotkeys
keyName="command+b,ctrl+b"
onKeyDown={() =>
!snap.compiling && snap.files.length && compileCode(snap.active)
}
>
<Flex
css={{
position: "absolute",
bottom: "$4",
left: "$4",
alignItems: "center",
display: "flex",
cursor: "pointer",
gap: "$2",
}}
>
<RunScript file={snap.files[snap.active]} />
</Flex>
</Hotkeys>
)}
</main>
<Box
css={{
display: "flex",
background: "$mauve1",
position: "relative",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Box>
<Flex css={{ width: "100%" }}>
<Flex
css={{
flex: 1,
background: "$mauve1",
position: "relative",
borderRight: "1px solid $mauve8",
}}
>
<LogBox
title="Development Log"
clearLog={() => (state.logs = [])}
logs={snap.logs}
/>
</Flex>
{snap.files[snap.active]?.name?.split(".")?.[1]?.toLowerCase() ===
"js" && (
<Flex
css={{
flex: 1,
}}
>
<LogBoxForScripts
showButtons={false}
title="Script Log"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
</Flex>
</Split>
);
};

View File

@@ -32,11 +32,15 @@ const Test = () => {
if (!showComponent) {
return null;
}
const hasScripts =
snap.files.filter((f) => f.name.endsWith(".js")).length > 0;
return (
<Container css={{ px: 0 }}>
<Split
direction="vertical"
sizes={getSplit("testVertical") || [50, 20, 30]}
sizes={
getSplit("testVertical") || (hasScripts ? [50, 20, 30] : [50, 50])
}
gutterSize={4}
gutterAlign="center"
style={{ height: "calc(100vh - 60px)" }}
@@ -91,16 +95,22 @@ const Test = () => {
</Box>
</Split>
</Flex>
<Flex
as="div"
css={{
borderTop: "1px solid $mauve6",
background: "$mauve1",
flexDirection: "column",
}}
>
<LogBoxForScripts title="Helper scripts" logs={snap.scriptLogs} />
</Flex>
{hasScripts && (
<Flex
as="div"
css={{
borderTop: "1px solid $mauve6",
background: "$mauve1",
flexDirection: "column",
}}
>
<LogBoxForScripts
title="Helper scripts"
logs={snap.scriptLogs}
clearLog={() => (state.scriptLogs = [])}
/>
</Flex>
)}
<Flex>
<Split
direction="horizontal"

View File

@@ -8,7 +8,8 @@ export const downloadAsZip = async () => {
state.zipLoading = true
// TODO do something about file/gist loading state
const files = state.files.map(({ name, content }) => ({ name, content }));
const zipped = await createZip(files);
const wasmFiles = state.files.filter(i => i.compiledContent).map(({ name, compiledContent }) => ({ name: `${name}.wasm`, content: compiledContent }));
const zipped = await createZip([...files, ...wasmFiles]);
const zipFileName = guessZipFileName(files);
zipped.saveFile(zipFileName);
} catch (error) {