Resuable tabs component and transaction tabs
This commit is contained in:
@@ -1,24 +1,58 @@
|
|||||||
import { useEffect, useState } from "react";
|
import React, { useEffect, useState, Fragment, isValidElement, useCallback } from "react";
|
||||||
import type { ReactNode, ReactElement } from "react";
|
import type { ReactNode, ReactElement } from "react";
|
||||||
import { Button, Stack } from ".";
|
import { Box, Button, Flex, Input, Stack, Text } from ".";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogClose,
|
||||||
|
} from "./Dialog";
|
||||||
|
import { Plus, X } from "phosphor-react";
|
||||||
|
import { styled } from "../stitches.config";
|
||||||
|
|
||||||
|
const ErrorText = styled(Text, {
|
||||||
|
color: "$red9",
|
||||||
|
mt: "$1",
|
||||||
|
display: "block",
|
||||||
|
});
|
||||||
|
|
||||||
interface TabProps {
|
interface TabProps {
|
||||||
header?: string;
|
header?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO customise strings shown
|
||||||
interface Props {
|
interface Props {
|
||||||
activeIndex?: number;
|
activeIndex?: number;
|
||||||
activeHeader?: string;
|
activeHeader?: string;
|
||||||
headless?: boolean;
|
headless?: boolean;
|
||||||
children: ReactElement<TabProps>[];
|
children: ReactElement<TabProps>[];
|
||||||
|
keepAllAlive?: boolean;
|
||||||
|
defaultExtension?: string;
|
||||||
|
onCreateNewTab?: (name: string) => any;
|
||||||
|
onCloseTab?: (index: number, header?: string) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tab = (props: TabProps) => null;
|
export const Tab = (props: TabProps) => null;
|
||||||
|
|
||||||
export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) => {
|
export const Tabs = ({
|
||||||
|
children,
|
||||||
|
activeIndex,
|
||||||
|
activeHeader,
|
||||||
|
headless,
|
||||||
|
keepAllAlive = false,
|
||||||
|
onCreateNewTab,
|
||||||
|
onCloseTab,
|
||||||
|
defaultExtension = "",
|
||||||
|
}: Props) => {
|
||||||
const [active, setActive] = useState(activeIndex || 0);
|
const [active, setActive] = useState(activeIndex || 0);
|
||||||
const tabProps: TabProps[] = children.map(elem => elem.props);
|
const tabs: TabProps[] = children.map(elem => elem.props);
|
||||||
|
|
||||||
|
const [isNewtabDialogOpen, setIsNewtabDialogOpen] = useState(false);
|
||||||
|
const [tabname, setTabname] = useState("");
|
||||||
|
const [newtabError, setNewtabError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeIndex) setActive(activeIndex);
|
if (activeIndex) setActive(activeIndex);
|
||||||
@@ -26,10 +60,53 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) =
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeHeader) {
|
if (activeHeader) {
|
||||||
const idx = tabProps.findIndex(tab => tab.header === activeHeader);
|
const idx = tabs.findIndex(tab => tab.header === activeHeader);
|
||||||
setActive(idx);
|
setActive(idx);
|
||||||
}
|
}
|
||||||
}, [activeHeader, tabProps]);
|
}, [activeHeader, tabs]);
|
||||||
|
|
||||||
|
// when filename changes, reset error
|
||||||
|
useEffect(() => {
|
||||||
|
setNewtabError(null);
|
||||||
|
}, [tabname, setNewtabError]);
|
||||||
|
|
||||||
|
const validateTabname = useCallback(
|
||||||
|
(tabname: string): { error: string | null } => {
|
||||||
|
if (tabs.find(tab => tab.header === tabname)) {
|
||||||
|
return { error: "Name already exists." };
|
||||||
|
}
|
||||||
|
return { error: null };
|
||||||
|
},
|
||||||
|
[tabs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateTab = useCallback(() => {
|
||||||
|
// add default extension in case omitted
|
||||||
|
let _tabname = tabname.includes(".") ? tabname : tabname + defaultExtension;
|
||||||
|
const chk = validateTabname(_tabname);
|
||||||
|
if (chk.error) {
|
||||||
|
setNewtabError(`Error: ${chk.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsNewtabDialogOpen(false);
|
||||||
|
setTabname("");
|
||||||
|
// switch to new tab?
|
||||||
|
setActive(tabs.length);
|
||||||
|
|
||||||
|
onCreateNewTab?.(_tabname);
|
||||||
|
}, [tabname, defaultExtension, validateTabname, onCreateNewTab, tabs.length]);
|
||||||
|
|
||||||
|
const handleCloseTab = useCallback(
|
||||||
|
(idx: number) => {
|
||||||
|
if (idx <= active && active !== 0) {
|
||||||
|
setActive(active - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseTab?.(idx, tabs[idx].header);
|
||||||
|
},
|
||||||
|
[active, onCloseTab, tabs]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -42,9 +119,9 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) =
|
|||||||
marginBottom: "-1px",
|
marginBottom: "-1px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{tabProps.map((prop, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<Button
|
<Button
|
||||||
key={prop.header}
|
key={tab.header}
|
||||||
role="tab"
|
role="tab"
|
||||||
tabIndex={idx}
|
tabIndex={idx}
|
||||||
onClick={() => setActive(idx)}
|
onClick={() => setActive(idx)}
|
||||||
@@ -59,12 +136,99 @@ export const Tabs = ({ children, activeIndex, activeHeader, headless }: Props) =
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{prop.header || idx}
|
{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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size="9px" weight="bold" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
{onCreateNewTab && (
|
||||||
|
<Dialog open={isNewtabDialogOpen} onOpenChange={setIsNewtabDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button ghost size="sm" css={{ alignItems: "center", px: "$2", mr: "$3" }}>
|
||||||
|
<Plus size="16px" /> {tabs.length === 0 && "Add new tab"}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogTitle>Create new tab</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<label>Tabname</label>
|
||||||
|
<Input
|
||||||
|
value={tabname}
|
||||||
|
onChange={e => setTabname(e.target.value)}
|
||||||
|
onKeyPress={e => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleCreateTab();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ErrorText>{newtabError}</ErrorText>
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
css={{
|
||||||
|
marginTop: 25,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
gap: "$3",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button outline>Cancel</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button variant="primary" onClick={handleCreateTab}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Box css={{ position: "absolute", top: "$3", right: "$3" }}>
|
||||||
|
<X size="20px" />
|
||||||
|
</Box>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{tabProps[active].children}
|
{keepAllAlive ? (
|
||||||
|
tabs.map((tab, idx) => {
|
||||||
|
// TODO Maybe rule out fragments as children
|
||||||
|
if (!isValidElement(tab.children)) {
|
||||||
|
if (active !== idx) return null;
|
||||||
|
return tab.children;
|
||||||
|
}
|
||||||
|
let key = tab.children.key || tab.header || idx;
|
||||||
|
let { children } = tab;
|
||||||
|
let { style, ...props } = children.props;
|
||||||
|
return (
|
||||||
|
<children.type
|
||||||
|
key={key}
|
||||||
|
{...props}
|
||||||
|
style={{ ...style, display: active !== idx ? "none" : undefined }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Fragment key={tabs[active].header || active}>{tabs[active].children}</Fragment>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import dynamic from "next/dynamic";
|
|||||||
import { useSnapshot } from "valtio";
|
import { useSnapshot } from "valtio";
|
||||||
import state from "../../state";
|
import state from "../../state";
|
||||||
import { sendTransaction } from "../../state/actions";
|
import { sendTransaction } from "../../state/actions";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState, FC } from "react";
|
||||||
import transactionsData from "../../content/transactions.json";
|
import transactionsData from "../../content/transactions.json";
|
||||||
|
|
||||||
const LogBox = dynamic(() => import("../../components/LogBox"), {
|
const LogBox = dynamic(() => import("../../components/LogBox"), {
|
||||||
@@ -18,7 +18,7 @@ const Accounts = dynamic(() => import("../../components/Accounts"), {
|
|||||||
type TxFields = Omit<typeof transactionsData[0], "Account" | "Sequence" | "TransactionType">;
|
type TxFields = Omit<typeof transactionsData[0], "Account" | "Sequence" | "TransactionType">;
|
||||||
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
|
type OtherFields = (keyof Omit<TxFields, "Destination">)[];
|
||||||
|
|
||||||
const Transaction = () => {
|
const Transaction: FC = props => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
|
|
||||||
const transactionsOptions = transactionsData.map(tx => ({
|
const transactionsOptions = transactionsData.map(tx => ({
|
||||||
@@ -150,7 +150,7 @@ const Transaction = () => {
|
|||||||
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
|
const usualFields = ["TransactionType", "Amount", "Account", "Destination"];
|
||||||
const otherFields = Object.keys(txFields).filter(k => !usualFields.includes(k)) as OtherFields;
|
const otherFields = Object.keys(txFields).filter(k => !usualFields.includes(k)) as OtherFields;
|
||||||
return (
|
return (
|
||||||
<Box css={{ position: "relative", height: "calc(100% - 28px)" }}>
|
<Box css={{ position: "relative", height: "calc(100% - 28px)" }} {...props}>
|
||||||
<Container css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}>
|
<Container css={{ p: "$3 0", fontSize: "$sm", height: "calc(100% - 28px)" }}>
|
||||||
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
|
<Flex column fluid css={{ height: "100%", overflowY: "auto" }}>
|
||||||
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
|
<Flex row fluid css={{ justifyContent: "flex-end", alignItems: "center", mb: "$3" }}>
|
||||||
@@ -280,6 +280,7 @@ const Transaction = () => {
|
|||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
const snap = useSnapshot(state);
|
const snap = useSnapshot(state);
|
||||||
|
const [tabHeaders, setTabHeaders] = useState<string[]>(["test1.json"]);
|
||||||
return (
|
return (
|
||||||
<Container css={{ py: "$3", px: 0 }}>
|
<Container css={{ py: "$3", px: 0 }}>
|
||||||
<Flex
|
<Flex
|
||||||
@@ -288,14 +289,17 @@ const Test = () => {
|
|||||||
css={{ justifyContent: "center", mb: "$2", height: "40vh", minHeight: "300px", p: "$3 $2" }}
|
css={{ justifyContent: "center", mb: "$2", height: "40vh", minHeight: "300px", p: "$3 $2" }}
|
||||||
>
|
>
|
||||||
<Box css={{ width: "60%", px: "$2", maxWidth: "800px", height: "100%", overflow: "auto" }}>
|
<Box css={{ width: "60%", px: "$2", maxWidth: "800px", height: "100%", overflow: "auto" }}>
|
||||||
<Tabs>
|
<Tabs
|
||||||
{/* TODO Dynamic tabs */}
|
keepAllAlive
|
||||||
<Tab header="test1.json">
|
defaultExtension=".json"
|
||||||
<Transaction />
|
onCreateNewTab={name => setTabHeaders(tabHeaders.concat(name))}
|
||||||
</Tab>
|
onCloseTab={index => setTabHeaders(tabHeaders.filter((_, idx) => idx !== index))}
|
||||||
<Tab header="test2.json">
|
>
|
||||||
<Transaction />
|
{tabHeaders.map(header => (
|
||||||
</Tab>
|
<Tab key={header} header={header}>
|
||||||
|
<Transaction />
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
<Box css={{ width: "40%", mx: "$2", height: "100%", maxWidth: "750px" }}>
|
<Box css={{ width: "40%", mx: "$2", height: "100%", maxWidth: "750px" }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user