mirror of
https://github.com/AykutSarac/jsoncrack.com.git
synced 2025-01-12 19:02:53 +08:00
feat: update ui
This commit is contained in:
parent
4895d235f7
commit
6ab9338709
@ -22,5 +22,6 @@
|
||||
]
|
||||
},
|
||||
"extends": ["next/core-web-vitals", "prettier"],
|
||||
"plugins": ["prettier", "unused-imports"]
|
||||
"plugins": ["prettier", "unused-imports"],
|
||||
"ignorePatterns": ["src/enums"]
|
||||
}
|
||||
|
@ -4,4 +4,4 @@ node_modules/
|
||||
out
|
||||
public
|
||||
*-lock.json
|
||||
tsconfig.json
|
||||
tsconfig.json
|
6
LICENSE
6
LICENSE
@ -631,8 +631,8 @@ to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
JSON Crack
|
||||
Copyright (C) 2023 Aykut Saraç
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
JSON Crack Copyright (C) 2023 Aykut Saraç
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
36
package.json
36
package.json
@ -2,6 +2,7 @@
|
||||
"name": "json-crack",
|
||||
"private": true,
|
||||
"version": "v3.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"author": "https://github.com/AykutSarac",
|
||||
"homepage": "https://jsoncrack.com",
|
||||
"scripts": {
|
||||
@ -19,22 +20,21 @@
|
||||
"@mantine/hooks": "^6.0.17",
|
||||
"@mantine/next": "^6.0.21",
|
||||
"@mantine/prism": "^6.0.21",
|
||||
"@monaco-editor/react": "^4.5.2",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@sentry/nextjs": "^7.72.0",
|
||||
"@supabase/auth-helpers-nextjs": "^0.8.1",
|
||||
"@supabase/auth-helpers-react": "^0.4.2",
|
||||
"@supabase/supabase-js": "^2.36.0",
|
||||
"@tanstack/react-query": "^4.35.3",
|
||||
"allotment": "^1.19.2",
|
||||
"allotment": "^1.19.3",
|
||||
"axios": "^1.5.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"html-to-image": "^1.11.11",
|
||||
"jq-in-the-browser": "^0.7.2",
|
||||
"jq-web": "^0.5.1",
|
||||
"json-2-csv": "^4.1.0",
|
||||
"json-2-csv": "^5.0.1",
|
||||
"json-to-ts": "^1.7.0",
|
||||
"jsonc-parser": "^3.2.0",
|
||||
"jsongraph-react": "^0.0.12",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jxon": "^2.0.0-beta.5",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
@ -46,35 +46,33 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-ga4": "^2.1.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-icons": "^4.12.0",
|
||||
"react-json-tree": "^0.18.0",
|
||||
"react-linkify-it": "^1.0.7",
|
||||
"react-simple-typewriter": "^5.0.1",
|
||||
"react-zoomable-ui": "^0.11.0",
|
||||
"reaflow": "5.2.6",
|
||||
"styled-components": "^6.0.8",
|
||||
"reaflow": "5.2.8",
|
||||
"styled-components": "^6.1.1",
|
||||
"toml": "^3.0.0",
|
||||
"use-long-press": "^3.1.5",
|
||||
"zustand": "^4.4.0"
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^13.4.12",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.3",
|
||||
"@types/jxon": "^2.0.2",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/lodash.get": "^4.4.7",
|
||||
"@types/lodash.set": "^4.3.7",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/jxon": "^2.0.5",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.set": "^4.3.9",
|
||||
"@types/node": "^20.4.7",
|
||||
"@types/react": "18.2.18",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.7",
|
||||
"eslint": "8.46.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-next": "13.4.12",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"prettier": "^3.0.1",
|
||||
"prettier": "^3.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import { MdLink, MdLinkOff } from "react-icons/md";
|
||||
import { CustomNodeProps } from "src/components/Graph/CustomNode";
|
||||
import useToggleHide from "src/hooks/useToggleHide";
|
||||
import { isContentImage } from "src/lib/utils/graph/calculateNodeSize";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useStored from "src/store/useStored";
|
||||
import { TextRenderer } from "./TextRenderer";
|
||||
import * as Styled from "./styles";
|
||||
|
||||
@ -52,13 +52,13 @@ const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) =>
|
||||
data: { isParent, childrenCount, type },
|
||||
} = node;
|
||||
const { validateHiddenNodes } = useToggleHide();
|
||||
const hideCollapse = useStored(state => state.hideCollapse);
|
||||
const showChildrenCount = useStored(state => state.childrenCount);
|
||||
const imagePreview = useStored(state => state.imagePreview);
|
||||
const collapseButtonVisible = useConfig(state => state.collapseButtonVisible);
|
||||
const childrenCountVisible = useConfig(state => state.childrenCountVisible);
|
||||
const imagePreviewEnabled = useConfig(state => state.imagePreviewEnabled);
|
||||
const expandNodes = useGraph(state => state.expandNodes);
|
||||
const collapseNodes = useGraph(state => state.collapseNodes);
|
||||
const isExpanded = useGraph(state => state.collapsedParents.includes(id));
|
||||
const isImage = imagePreview && isContentImage(text as string);
|
||||
const isImage = imagePreviewEnabled && isContentImage(text as string);
|
||||
const value = JSON.stringify(text).replaceAll('"', "");
|
||||
|
||||
const handleExpand = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@ -80,16 +80,16 @@ const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) =>
|
||||
data-x={x}
|
||||
data-y={y}
|
||||
data-key={JSON.stringify(text)}
|
||||
$hasCollapse={isParent && hideCollapse}
|
||||
$hasCollapse={isParent && collapseButtonVisible}
|
||||
>
|
||||
<Styled.StyledKey $value={value} $parent={isParent} $type={type}>
|
||||
<TextRenderer>{value}</TextRenderer>
|
||||
</Styled.StyledKey>
|
||||
{isParent && childrenCount > 0 && showChildrenCount && (
|
||||
{isParent && childrenCount > 0 && childrenCountVisible && (
|
||||
<Styled.StyledChildrenCount>({childrenCount})</Styled.StyledChildrenCount>
|
||||
)}
|
||||
|
||||
{isParent && hasCollapse && hideCollapse && (
|
||||
{isParent && hasCollapse && collapseButtonVisible && (
|
||||
<StyledExpand aria-label="Expand" onClick={handleExpand}>
|
||||
{isExpanded ? <MdLinkOff size={18} /> : <MdLink size={18} />}
|
||||
</StyledExpand>
|
||||
|
@ -25,15 +25,15 @@ const isURL =
|
||||
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi;
|
||||
|
||||
export const TextRenderer: React.FC<{ children: string }> = ({ children }) => {
|
||||
if (isURL.test(children.replaceAll('"', ""))) {
|
||||
if (isURL.test(children?.replaceAll('"', ""))) {
|
||||
return <Styled.StyledLinkItUrl>{children}</Styled.StyledLinkItUrl>;
|
||||
}
|
||||
|
||||
if (isColorFormat(children.replaceAll('"', ""))) {
|
||||
if (isColorFormat(children?.replaceAll('"', ""))) {
|
||||
return (
|
||||
<StyledRow>
|
||||
<ColorSwatch radius={4} h={12} w={12} color={children.replaceAll('"', "")} />
|
||||
{children.replaceAll('"', "")}
|
||||
<ColorSwatch radius={4} h={12} w={12} mr={8} color={children?.replaceAll('"', "")} />
|
||||
{children?.replaceAll('"', "")}
|
||||
</StyledRow>
|
||||
);
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import React from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import styled from "styled-components";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { Space } from "react-zoomable-ui";
|
||||
import { ElkRoot } from "reaflow/dist/layout/useLayout";
|
||||
import { useLongPress } from "use-long-press";
|
||||
import { CustomNode } from "src/components/Graph/CustomNode";
|
||||
import { ViewMode } from "src/enums/viewMode.enum";
|
||||
import useToggleHide from "src/hooks/useToggleHide";
|
||||
import { Loading } from "src/layout/Loading";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useStored from "src/store/useStored";
|
||||
import useUser from "src/store/useUser";
|
||||
import { NodeData } from "src/types/graph";
|
||||
import { CustomEdge } from "./CustomEdge";
|
||||
@ -23,7 +25,7 @@ interface GraphProps {
|
||||
isWidget?: boolean;
|
||||
}
|
||||
|
||||
const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
|
||||
const StyledEditorWrapper = styled.div<{ $widget: boolean; $showRulers: boolean }>`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: ${({ $widget }) => ($widget ? "calc(100vh - 36px)" : "calc(100vh - 63px)")};
|
||||
@ -33,20 +35,24 @@ const StyledEditorWrapper = styled.div<{ $widget: boolean }>`
|
||||
--line-color-2: ${({ theme }) => theme.GRID_COLOR_SECONDARY};
|
||||
|
||||
background-color: var(--bg-color);
|
||||
background-image: linear-gradient(var(--line-color-1) 1.5px, transparent 1.5px),
|
||||
linear-gradient(90deg, var(--line-color-1) 1.5px, transparent 1.5px),
|
||||
linear-gradient(var(--line-color-2) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--line-color-2) 1px, transparent 1px);
|
||||
background-position:
|
||||
-1.5px -1.5px,
|
||||
-1.5px -1.5px,
|
||||
-1px -1px,
|
||||
-1px -1px;
|
||||
background-size:
|
||||
100px 100px,
|
||||
100px 100px,
|
||||
20px 20px,
|
||||
20px 20px;
|
||||
${({ $showRulers }) =>
|
||||
$showRulers &&
|
||||
`
|
||||
background-image: linear-gradient(var(--line-color-1) 1.5px, transparent 1.5px),
|
||||
linear-gradient(90deg, var(--line-color-1) 1.5px, transparent 1.5px),
|
||||
linear-gradient(var(--line-color-2) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--line-color-2) 1px, transparent 1px);
|
||||
background-position:
|
||||
-1.5px -1.5px,
|
||||
-1.5px -1.5px,
|
||||
-1px -1px,
|
||||
-1px -1px;
|
||||
background-size:
|
||||
100px 100px,
|
||||
100px 100px,
|
||||
20px 20px,
|
||||
20px 20px;
|
||||
`};
|
||||
|
||||
:active {
|
||||
cursor: move;
|
||||
@ -76,7 +82,8 @@ const layoutOptions = {
|
||||
};
|
||||
|
||||
const PREMIUM_LIMIT = 200;
|
||||
const ERROR_LIMIT = 3_000;
|
||||
const ERROR_LIMIT_TREE = 5_000;
|
||||
const ERROR_LIMIT = 10_000;
|
||||
|
||||
const GraphCanvas = ({ isWidget }: GraphProps) => {
|
||||
const { validateHiddenNodes } = useToggleHide();
|
||||
@ -138,6 +145,7 @@ const GraphCanvas = ({ isWidget }: GraphProps) => {
|
||||
|
||||
function getViewType(nodes: NodeData[]) {
|
||||
if (nodes.length > ERROR_LIMIT) return "error";
|
||||
if (nodes.length > ERROR_LIMIT_TREE) return "tree";
|
||||
if (nodes.length > PREMIUM_LIMIT) return "premium";
|
||||
return "graph";
|
||||
}
|
||||
@ -147,7 +155,9 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
|
||||
const loading = useGraph(state => state.loading);
|
||||
const isPremium = useUser(state => state.premium);
|
||||
const viewType = useGraph(state => getViewType(state.nodes));
|
||||
const gesturesEnabled = useStored(state => state.gesturesEnabled);
|
||||
const gesturesEnabled = useConfig(state => state.gesturesEnabled);
|
||||
const rulersEnabled = useConfig(state => state.rulersEnabled);
|
||||
const setViewMode = useConfig(state => state.setViewMode);
|
||||
|
||||
const callback = React.useCallback(() => {
|
||||
const canvas = document.querySelector(".jsoncrack-canvas") as HTMLDivElement | null;
|
||||
@ -166,7 +176,14 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
|
||||
if ("activeElement" in document) (document.activeElement as HTMLElement)?.blur();
|
||||
}, []);
|
||||
|
||||
if (viewType === "error") return <ErrorView />;
|
||||
if (viewType === "error") {
|
||||
return <ErrorView />;
|
||||
}
|
||||
|
||||
if (viewType === "tree") {
|
||||
setViewMode(ViewMode.Tree);
|
||||
toast("This document is too large to display as a graph. Switching to tree view.");
|
||||
}
|
||||
|
||||
if (viewType === "premium" && !isWidget) {
|
||||
if (!isPremium) return <PremiumView />;
|
||||
@ -180,6 +197,7 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
|
||||
onContextMenu={e => e.preventDefault()}
|
||||
onClick={blurOnClick}
|
||||
key={String(gesturesEnabled)}
|
||||
$showRulers={rulersEnabled}
|
||||
{...bindLongPress()}
|
||||
>
|
||||
<Space
|
||||
|
@ -2,8 +2,8 @@ import React from "react";
|
||||
import styled from "styled-components";
|
||||
import Editor, { loader, useMonaco } from "@monaco-editor/react";
|
||||
import { Loading } from "src/layout/Loading";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useFile from "src/store/useFile";
|
||||
import useStored from "src/store/useStored";
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
@ -33,7 +33,7 @@ export const MonacoEditor = () => {
|
||||
const setError = useFile(state => state.setError);
|
||||
const jsonSchema = useFile(state => state.jsonSchema);
|
||||
const getHasChanges = useFile(state => state.getHasChanges);
|
||||
const theme = useStored(state => (state.lightmode ? "light" : "vs-dark"));
|
||||
const theme = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
|
||||
const fileType = useFile(state => state.format);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -11,22 +11,15 @@ import {
|
||||
AiOutlineLock,
|
||||
AiOutlineUnlock,
|
||||
} from "react-icons/ai";
|
||||
import { BiSolidDockLeft } from "react-icons/bi";
|
||||
import { MdOutlineCheckCircleOutline } from "react-icons/md";
|
||||
import { TbTransform } from "react-icons/tb";
|
||||
import {
|
||||
VscAccount,
|
||||
VscError,
|
||||
VscFeedback,
|
||||
VscSourceControl,
|
||||
VscSync,
|
||||
VscSyncIgnored,
|
||||
VscWorkspaceTrusted,
|
||||
} from "react-icons/vsc";
|
||||
import { VscError, VscFeedback, VscSourceControl, VscSync, VscSyncIgnored } from "react-icons/vsc";
|
||||
import { documentSvc } from "src/services/document.service";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useFile from "src/store/useFile";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useModal from "src/store/useModal";
|
||||
import useStored from "src/store/useStored";
|
||||
import useUser from "src/store/useUser";
|
||||
|
||||
const StyledBottomBar = styled.div`
|
||||
@ -51,6 +44,7 @@ const StyledLeft = styled.div`
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
gap: 4px;
|
||||
padding-left: 8px;
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
display: none;
|
||||
@ -95,15 +89,16 @@ export const BottomBar = () => {
|
||||
const { query, replace } = useRouter();
|
||||
const data = useFile(state => state.fileData);
|
||||
const user = useUser(state => state.user);
|
||||
const premium = useUser(state => state.premium);
|
||||
const toggleLiveTransform = useStored(state => state.toggleLiveTransform);
|
||||
const liveTransform = useStored(state => state.liveTransform);
|
||||
const toggleLiveTransform = useConfig(state => state.toggleLiveTransform);
|
||||
const liveTransformEnabled = useConfig(state => state.liveTransformEnabled);
|
||||
const hasChanges = useFile(state => state.hasChanges);
|
||||
const error = useFile(state => state.error);
|
||||
const getContents = useFile(state => state.getContents);
|
||||
const setContents = useFile(state => state.setContents);
|
||||
const nodeCount = useGraph(state => state.nodes.length);
|
||||
const fileName = useFile(state => state.fileData?.name);
|
||||
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
|
||||
const fullscreen = useGraph(state => state.fullscreen);
|
||||
|
||||
const setVisible = useModal(state => state.setVisible);
|
||||
const setHasChanges = useFile(state => state.setHasChanges);
|
||||
@ -111,6 +106,8 @@ export const BottomBar = () => {
|
||||
const [isPrivate, setIsPrivate] = React.useState(false);
|
||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||
|
||||
const toggleEditor = () => toggleFullscreen(!fullscreen);
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsPrivate(data?.private ?? true);
|
||||
}, [data]);
|
||||
@ -191,20 +188,10 @@ export const BottomBar = () => {
|
||||
</Head>
|
||||
)}
|
||||
<StyledLeft>
|
||||
<StyledBottomBarItem $bg="#1864AB" onClick={handleLoginClick}>
|
||||
<Flex align="center" gap={5} px={5}>
|
||||
<VscAccount color="white" />
|
||||
<Text maw={120} c="white" truncate="end">
|
||||
{user?.user_metadata.name ?? "Login"}
|
||||
</Text>
|
||||
</Flex>
|
||||
<StyledBottomBarItem onClick={toggleEditor}>
|
||||
<BiSolidDockLeft />
|
||||
</StyledBottomBarItem>
|
||||
{!premium && (
|
||||
<StyledBottomBarItem onClick={() => setVisible("premium")(true)}>
|
||||
<VscWorkspaceTrusted />
|
||||
Upgrade to Premium
|
||||
</StyledBottomBarItem>
|
||||
)}
|
||||
|
||||
{fileName && (
|
||||
<StyledBottomBarItem onClick={() => setVisible("cloud")(true)}>
|
||||
<VscSourceControl />
|
||||
@ -236,7 +223,7 @@ export const BottomBar = () => {
|
||||
{(data?.owner_email === user?.email || (!data && user)) && (
|
||||
<StyledBottomBarItem onClick={handleSaveJson} disabled={isUpdating || error}>
|
||||
{hasChanges ? <AiOutlineCloudUpload /> : <AiOutlineCloudSync />}
|
||||
{hasChanges ? (query?.json ? "Unsaved Changes" : "Create Document") : "Saved"}
|
||||
{hasChanges || !user ? (query?.json ? "Unsaved Changes" : "Save to Cloud") : "Saved"}
|
||||
</StyledBottomBarItem>
|
||||
)}
|
||||
{data?.owner_email === user?.email && (
|
||||
@ -252,7 +239,7 @@ export const BottomBar = () => {
|
||||
<AiOutlineLink />
|
||||
Share
|
||||
</StyledBottomBarItem>
|
||||
{liveTransform ? (
|
||||
{liveTransformEnabled ? (
|
||||
<StyledBottomBarItem onClick={() => toggleLiveTransform(false)}>
|
||||
<VscSync />
|
||||
<Text>Live Transform</Text>
|
||||
@ -263,7 +250,7 @@ export const BottomBar = () => {
|
||||
<Text>Manual Transform</Text>
|
||||
</StyledBottomBarItem>
|
||||
)}
|
||||
{!liveTransform && (
|
||||
{!liveTransformEnabled && (
|
||||
<StyledBottomBarItem onClick={() => setContents({})}>
|
||||
<TbTransform />
|
||||
Transform
|
||||
|
@ -123,7 +123,7 @@ const PromptInput = () => {
|
||||
export const JsonEditor: React.FC = () => {
|
||||
return (
|
||||
<StyledEditorWrapper>
|
||||
<PromptInput />
|
||||
{/* <PromptInput /> */}
|
||||
<MonacoEditor />
|
||||
</StyledEditorWrapper>
|
||||
);
|
||||
|
@ -1,36 +1,51 @@
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import styled from "styled-components";
|
||||
import { Flex, Group, MediaQuery, Menu, Select, Text } from "@mantine/core";
|
||||
import { useHotkeys } from "@mantine/hooks";
|
||||
import {
|
||||
Avatar,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
MediaQuery,
|
||||
Menu,
|
||||
SegmentedControl,
|
||||
Select,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { getHotkeyHandler, useHotkeys } from "@mantine/hooks";
|
||||
import ReactGA from "react-ga4";
|
||||
import toast from "react-hot-toast";
|
||||
import { AiOutlineFullscreen, AiOutlineMinus, AiOutlinePlus } from "react-icons/ai";
|
||||
import { AiOutlineFullscreen } from "react-icons/ai";
|
||||
import { BsCheck2 } from "react-icons/bs";
|
||||
import { CgArrowsMergeAltH, CgArrowsShrinkH, CgChevronDown } from "react-icons/cg";
|
||||
import { FiDownload } from "react-icons/fi";
|
||||
import { MdCenterFocusWeak } from "react-icons/md";
|
||||
import { MdOutlineWorkspacePremium } from "react-icons/md";
|
||||
import { SiJsonwebtokens } from "react-icons/si";
|
||||
import { TiFlowMerge } from "react-icons/ti";
|
||||
import {
|
||||
VscCollapseAll,
|
||||
VscExpandAll,
|
||||
VscJson,
|
||||
VscLayoutSidebarLeft,
|
||||
VscLayoutSidebarLeftOff,
|
||||
VscSettingsGear,
|
||||
VscTarget,
|
||||
VscSearchFuzzy,
|
||||
VscGroupByRefType,
|
||||
VscSignOut,
|
||||
VscFeedback,
|
||||
VscSignIn,
|
||||
} from "react-icons/vsc";
|
||||
import { SearchInput } from "src/components/SearchInput";
|
||||
import { FileFormat } from "src/enums/file";
|
||||
import { FileFormat } from "src/enums/file.enum";
|
||||
import { ViewMode } from "src/enums/viewMode.enum";
|
||||
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
|
||||
import { getNextDirection } from "src/lib/utils/graph/getNextDirection";
|
||||
import { isIframe } from "src/lib/utils/widget";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useFile from "src/store/useFile";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useJC from "src/store/useJC";
|
||||
import useJson from "src/store/useJson";
|
||||
import useModal from "src/store/useModal";
|
||||
import useUser from "src/store/useUser";
|
||||
|
||||
export const StyledTools = styled.div`
|
||||
position: relative;
|
||||
@ -94,17 +109,13 @@ function fullscreenBrowser() {
|
||||
const ViewMenu = () => {
|
||||
const [coreKey, setCoreKey] = React.useState("CTRL");
|
||||
const toggleFold = useGraph(state => state.toggleFold);
|
||||
const setDirection = useJC(state => state.graphRef?.setDirection);
|
||||
const direction = useJC(state => state.graphRef?.direction);
|
||||
const expandGraph = useJC(state => state.graphRef?.expandGraph);
|
||||
const collapseGraph = useJC(state => state.graphRef?.collapseGraph);
|
||||
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
|
||||
const focusFirstNode = useJC(state => state.graphRef?.focusFirstNode);
|
||||
const setDirection = useGraph(state => state.setDirection);
|
||||
const direction = useGraph(state => state.direction);
|
||||
const expandGraph = useGraph(state => state.expandGraph);
|
||||
const collapseGraph = useGraph(state => state.collapseGraph);
|
||||
const focusFirstNode = useGraph(state => state.focusFirstNode);
|
||||
const foldNodes = useGraph(state => state.foldNodes);
|
||||
const graphCollapsed = useJC(state => state.graphRef?.graphCollapsed);
|
||||
const fullscreen = useGraph(state => state.fullscreen);
|
||||
|
||||
const toggleEditor = () => toggleFullscreen(!fullscreen);
|
||||
const graphCollapsed = useGraph(state => state.graphCollapsed);
|
||||
|
||||
const toggleFoldNodes = () => {
|
||||
toggleFold(!foldNodes);
|
||||
@ -124,7 +135,6 @@ const ViewMenu = () => {
|
||||
};
|
||||
|
||||
useHotkeys([
|
||||
["mod+shift+e", toggleEditor],
|
||||
["mod+shift+d", toggleDirection],
|
||||
["mod+shift+f", toggleFoldNodes],
|
||||
["mod+shift+c", toggleExpandCollapseGraph],
|
||||
@ -144,7 +154,7 @@ const ViewMenu = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu shadow="md" closeOnItemClick={false}>
|
||||
<Menu shadow="md" closeOnItemClick={false} withArrow>
|
||||
<Menu.Target>
|
||||
<StyledToolElement>
|
||||
<Flex align="center" gap={3}>
|
||||
@ -153,25 +163,6 @@ const ViewMenu = () => {
|
||||
</StyledToolElement>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
fz={12}
|
||||
onClick={() => {
|
||||
toggleEditor();
|
||||
ReactGA.event({
|
||||
action: "toggle_hide_editor",
|
||||
category: "User",
|
||||
label: "Tools",
|
||||
});
|
||||
}}
|
||||
icon={fullscreen ? <VscLayoutSidebarLeft /> : <VscLayoutSidebarLeftOff />}
|
||||
rightSection={
|
||||
<Text ml="md" fz={10} color="dimmed">
|
||||
{coreKey} Shift E
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
{fullscreen ? "Show" : "Hide"} Editor
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
fz={12}
|
||||
onClick={() => {
|
||||
@ -238,16 +229,50 @@ const ViewMenu = () => {
|
||||
};
|
||||
|
||||
export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => {
|
||||
const { push } = useRouter();
|
||||
|
||||
const getJson = useJson(state => state.getJson);
|
||||
const setVisible = useModal(state => state.setVisible);
|
||||
const centerView = useJC(state => state.graphRef?.centerView);
|
||||
const zoomIn = useJC(state => state.graphRef?.zoomIn);
|
||||
const zoomOut = useJC(state => state.graphRef?.zoomOut);
|
||||
const centerView = useGraph(state => state.centerView);
|
||||
const zoomIn = useGraph(state => state.zoomIn);
|
||||
const zoomOut = useGraph(state => state.zoomOut);
|
||||
const toggleGestures = useConfig(state => state.toggleGestures);
|
||||
const toggleChildrenCount = useConfig(state => state.toggleChildrenCount);
|
||||
const toggleDarkMode = useConfig(state => state.toggleDarkMode);
|
||||
const toggleRulers = useConfig(state => state.toggleRulers);
|
||||
const toggleCollapseButton = useConfig(state => state.toggleCollapseButton);
|
||||
const setViewMode = useConfig(state => state.setViewMode);
|
||||
const setZoomFactor = useGraph(state => state.setZoomFactor);
|
||||
const toggleImagePreview = useConfig(state => state.toggleImagePreview);
|
||||
const logout = useUser(state => state.logout);
|
||||
|
||||
const user = useUser(state => state.user);
|
||||
const premium = useUser(state => state.premium);
|
||||
|
||||
const zoomFactor = useGraph(state => state.viewPort?.zoomFactor || 1);
|
||||
const gesturesEnabled = useConfig(state => state.gesturesEnabled);
|
||||
const childrenCountVisible = useConfig(state => state.childrenCountVisible);
|
||||
const darkmodeEnabled = useConfig(state => state.darkmodeEnabled);
|
||||
const viewMode = useConfig(state => state.viewMode);
|
||||
const rulersEnabled = useConfig(state => state.rulersEnabled);
|
||||
const collapseButtonVisible = useConfig(state => state.collapseButtonVisible);
|
||||
const imagePreviewEnabled = useConfig(state => state.imagePreviewEnabled);
|
||||
|
||||
const setFormat = useFile(state => state.setFormat);
|
||||
const format = useFile(state => state.format);
|
||||
|
||||
const [tempZoomValue, setTempZoomValue] = React.useState(zoomFactor);
|
||||
const [logoURL, setLogoURL] = React.useState("CTRL");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!Number.isNaN(zoomFactor)) setTempZoomValue(zoomFactor);
|
||||
}, [zoomFactor]);
|
||||
|
||||
useHotkeys([
|
||||
["shift+Digit0", () => setZoomFactor(100 / 100)],
|
||||
["shift+Digit1", centerView],
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const url = !isIframe()
|
||||
@ -303,11 +328,30 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
|
||||
{ value: FileFormat.CSV, label: "CSV" },
|
||||
]}
|
||||
/>
|
||||
<Menu trigger="click" shadow="md" withArrow>
|
||||
<Menu.Target>
|
||||
<StyledToolElement title="View Mode">
|
||||
<Flex align="center" gap={3}>
|
||||
View Mode <CgChevronDown />
|
||||
</Flex>
|
||||
</StyledToolElement>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<SegmentedControl
|
||||
value={viewMode}
|
||||
onChange={setViewMode}
|
||||
data={[
|
||||
{ value: ViewMode.Graph, label: "Graph" },
|
||||
{ value: ViewMode.Tree, label: "Tree" },
|
||||
]}
|
||||
/>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<StyledToolElement title="Import File" onClick={() => setVisible("import")(true)}>
|
||||
Import
|
||||
</StyledToolElement>
|
||||
<ViewMenu />
|
||||
<Menu shadow="md">
|
||||
<Menu shadow="md" withArrow>
|
||||
<Menu.Target>
|
||||
<StyledToolElement>
|
||||
<Flex align="center" gap={3}>
|
||||
@ -348,31 +392,170 @@ export const Tools: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) =>
|
||||
</MediaQuery>
|
||||
)}
|
||||
<Group spacing="xs" position="right" w="100%" noWrap>
|
||||
<StyledToolElement title="Zoom Out" onClick={zoomOut}>
|
||||
<AiOutlineMinus size="18" />
|
||||
</StyledToolElement>
|
||||
<StyledToolElement title="Zoom In" onClick={zoomIn}>
|
||||
<AiOutlinePlus size="18" />
|
||||
</StyledToolElement>
|
||||
<SearchInput />
|
||||
|
||||
{!isWidget && (
|
||||
<StyledToolElement title="Save as Image" onClick={() => setVisible("download")(true)}>
|
||||
<FiDownload size="18" />
|
||||
</StyledToolElement>
|
||||
)}
|
||||
<StyledToolElement title="Center Canvas" onClick={centerView}>
|
||||
<MdCenterFocusWeak size="18" />
|
||||
</StyledToolElement>
|
||||
<SearchInput />
|
||||
<StyledToolElement title="Fullscreen" $hide={isWidget} onClick={fullscreenBrowser}>
|
||||
<AiOutlineFullscreen size="18" />
|
||||
</StyledToolElement>
|
||||
<StyledToolElement
|
||||
title="Settings"
|
||||
$hide={isWidget}
|
||||
onClick={() => setVisible("settings")(true)}
|
||||
>
|
||||
<VscSettingsGear size="18" />
|
||||
</StyledToolElement>
|
||||
|
||||
{!isWidget && (
|
||||
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
|
||||
<Menu.Target>
|
||||
<StyledToolElement>
|
||||
<Flex gap={4}>
|
||||
{Math.round(zoomFactor * 100)}%
|
||||
<CgChevronDown />
|
||||
</Flex>
|
||||
</StyledToolElement>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item>
|
||||
<Input
|
||||
type="number"
|
||||
value={Math.round(tempZoomValue * 100)}
|
||||
onChange={e => setTempZoomValue(e.currentTarget.valueAsNumber / 100)}
|
||||
onKeyDown={getHotkeyHandler([["Enter", () => setZoomFactor(tempZoomValue)]])}
|
||||
size="xs"
|
||||
rightSection="%"
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item rightSection="+" onClick={zoomIn}>
|
||||
<Text size="xs">Zoom in</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item rightSection="-" onClick={zoomOut}>
|
||||
<Text size="xs">Zoom out</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item rightSection="⇧ 1" onClick={centerView}>
|
||||
<Text size="xs">Zoom to fit</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => setZoomFactor(50 / 100)}>
|
||||
<Text size="xs">Zoom to %50</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item rightSection="⇧ 0" onClick={() => setZoomFactor(100 / 100)}>
|
||||
<Text size="xs">Zoom to %100</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => setZoomFactor(200 / 100)}>
|
||||
<Text size="xs">Zoom to %200</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
icon={<BsCheck2 opacity={rulersEnabled ? 100 : 0} />}
|
||||
onClick={() => toggleRulers(!rulersEnabled)}
|
||||
>
|
||||
<Text size="xs">Rulers</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<BsCheck2 opacity={gesturesEnabled ? 100 : 0} />}
|
||||
onClick={() => toggleGestures(!gesturesEnabled)}
|
||||
>
|
||||
<Text size="xs">Trackpad Gestures</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<BsCheck2 opacity={childrenCountVisible ? 100 : 0} />}
|
||||
onClick={() => toggleChildrenCount(!childrenCountVisible)}
|
||||
>
|
||||
<Text size="xs">Item Count</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<BsCheck2 opacity={imagePreviewEnabled ? 100 : 0} />}
|
||||
onClick={() => toggleImagePreview(!imagePreviewEnabled)}
|
||||
>
|
||||
<Text size="xs">Image Link Preview</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<BsCheck2 opacity={collapseButtonVisible ? 100 : 0} />}
|
||||
onClick={() => toggleCollapseButton(!collapseButtonVisible)}
|
||||
>
|
||||
<Text size="xs">Show Expand/Collapse</Text>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{!isWidget && (
|
||||
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
|
||||
<Menu.Target>
|
||||
<StyledToolElement>
|
||||
<Avatar
|
||||
color="grape"
|
||||
variant="filled"
|
||||
size={20}
|
||||
radius="xl"
|
||||
src={user?.user_metadata.avatar_url}
|
||||
alt={user?.user_metadata.name}
|
||||
>
|
||||
{user?.user_metadata.name[0]}
|
||||
</Avatar>
|
||||
</StyledToolElement>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{user ? (
|
||||
<Menu.Item
|
||||
icon={
|
||||
<Avatar color="grape" alt={user.user_metadata.name} size={20} radius="xl">
|
||||
{user?.user_metadata.name[0]}
|
||||
</Avatar>
|
||||
}
|
||||
onClick={() => setVisible("account")(true)}
|
||||
closeMenuOnClick
|
||||
>
|
||||
<Text size="xs">Account</Text>
|
||||
</Menu.Item>
|
||||
) : (
|
||||
<Link href="/sign-in">
|
||||
<Menu.Item icon={<VscSignIn />}>
|
||||
<Text size="xs">Sign in</Text>
|
||||
</Menu.Item>
|
||||
</Link>
|
||||
)}
|
||||
{!premium && (
|
||||
<Menu.Item
|
||||
icon={<MdOutlineWorkspacePremium color="red" />}
|
||||
onClick={() => setVisible("premium")(true)}
|
||||
closeMenuOnClick
|
||||
>
|
||||
<Text
|
||||
variant="gradient"
|
||||
fw="bold"
|
||||
gradient={{ from: "orange", to: "red" }}
|
||||
size="xs"
|
||||
>
|
||||
Get Premium
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
icon={<BsCheck2 opacity={darkmodeEnabled ? 100 : 0} />}
|
||||
onClick={() => toggleDarkMode(!darkmodeEnabled)}
|
||||
>
|
||||
<Text size="xs">Dark Mode</Text>
|
||||
</Menu.Item>
|
||||
{user && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
icon={<VscFeedback />}
|
||||
onClick={() => setVisible("review")(true)}
|
||||
closeMenuOnClick
|
||||
>
|
||||
<Text size="xs">Feedback</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<VscSignOut />} onClick={() => logout()} closeMenuOnClick>
|
||||
<Text size="xs">Log out</Text>
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{!isWidget && (
|
||||
<StyledToolElement title="Fullscreen" $hide={isWidget} onClick={fullscreenBrowser}>
|
||||
<AiOutlineFullscreen size="18" />
|
||||
</StyledToolElement>
|
||||
)}
|
||||
</Group>
|
||||
</StyledTools>
|
||||
);
|
||||
|
@ -1,87 +1,145 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { JSONGraph } from "jsongraph-react";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useJC from "src/store/useJC";
|
||||
import styled, { DefaultTheme, useTheme } from "styled-components";
|
||||
import { Menu, Text } from "@mantine/core";
|
||||
import { JSONTree } from "react-json-tree";
|
||||
import { Graph } from "src/components/Graph";
|
||||
import { TextRenderer } from "src/components/Graph/CustomNode/TextRenderer";
|
||||
import { firaMono } from "src/constants/fonts";
|
||||
import { ViewMode } from "src/enums/viewMode.enum";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useJson from "src/store/useJson";
|
||||
import useModal from "src/store/useModal";
|
||||
import useStored from "src/store/useStored";
|
||||
import { NodeData } from "src/types/graph";
|
||||
|
||||
const StyledLiveEditor = styled.div`
|
||||
position: relative;
|
||||
height: 100%;
|
||||
background: ${({ theme }) => theme.GRID_BG_COLOR};
|
||||
overflow: auto;
|
||||
|
||||
& > ul {
|
||||
margin-top: 0 !important;
|
||||
padding: 12px !important;
|
||||
font-family: ${firaMono.style.fontFamily};
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
const LiveEditor: React.FC = () => {
|
||||
const lightmode = useStored(state => state.lightmode);
|
||||
type TextColorFn = {
|
||||
theme: DefaultTheme;
|
||||
$value?: string | unknown;
|
||||
};
|
||||
|
||||
function getValueColor({ $value, theme }: TextColorFn) {
|
||||
if ($value && !Number.isNaN(+$value)) return theme.NODE_COLORS.INTEGER;
|
||||
if ($value === "true") return theme.NODE_COLORS.BOOL.TRUE;
|
||||
if ($value === "false") return theme.NODE_COLORS.BOOL.FALSE;
|
||||
if ($value === "null") return theme.NODE_COLORS.NULL;
|
||||
|
||||
// default
|
||||
return theme.NODE_COLORS.NODE_VALUE;
|
||||
}
|
||||
|
||||
function getLabelColor({ $type, theme }: { $type?: string; theme: DefaultTheme }) {
|
||||
if ($type === "Object") return theme.NODE_COLORS.PARENT_OBJ;
|
||||
if ($type === "Array") return theme.NODE_COLORS.PARENT_ARR;
|
||||
return theme.NODE_COLORS.PARENT_OBJ;
|
||||
}
|
||||
|
||||
const View = () => {
|
||||
const theme = useTheme();
|
||||
const json = useJson(state => state.json);
|
||||
const setJCRef = useJC(state => state.setJCRef);
|
||||
const graphRef = useJC(state => state.graphRef);
|
||||
const setSelectedNode = useGraph(state => state.setSelectedNode);
|
||||
const setVisible = useModal(state => state.setVisible);
|
||||
const viewMode = useConfig(state => state.viewMode);
|
||||
|
||||
console.log(graphRef);
|
||||
if (viewMode === ViewMode.Graph) return <Graph />;
|
||||
|
||||
const handleNodeClick = React.useCallback(
|
||||
(data: NodeData) => {
|
||||
if (setSelectedNode) setSelectedNode(data);
|
||||
setVisible("node")(true);
|
||||
},
|
||||
[setSelectedNode, setVisible]
|
||||
);
|
||||
if (viewMode === ViewMode.Tree)
|
||||
return (
|
||||
<JSONTree
|
||||
data={JSON.parse(json)}
|
||||
labelRenderer={(keyPath, nodeType, expanded, expandable) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: getLabelColor({
|
||||
theme,
|
||||
$type: nodeType,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{keyPath[0]}:
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
valueRenderer={(valueAsString, value) => {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
color: getValueColor({
|
||||
theme,
|
||||
$value: valueAsString,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<TextRenderer>{JSON.stringify(value)}</TextRenderer>
|
||||
</span>
|
||||
);
|
||||
}}
|
||||
theme={{
|
||||
extend: {
|
||||
overflow: "scroll",
|
||||
height: "100%",
|
||||
scheme: "monokai",
|
||||
author: "wimer hazenberg (http://www.monokai.nl)",
|
||||
base00: theme.GRID_BG_COLOR,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const style = React.useMemo(
|
||||
() => ({
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleLayoutChange = React.useCallback(() => {
|
||||
// graphRef shouldn't be null here
|
||||
console.log(graphRef);
|
||||
setTimeout(graphRef?.centerView, 100);
|
||||
}, [graphRef]);
|
||||
|
||||
const layout = React.useMemo(
|
||||
() => ({
|
||||
touchGestures: true,
|
||||
theme: lightmode ? "light" : ("dark" as any),
|
||||
}),
|
||||
[lightmode]
|
||||
);
|
||||
const LiveEditor: React.FC = () => {
|
||||
const viewMode = useConfig(state => state.viewMode);
|
||||
const [contextOpened, setContextOpened] = React.useState(false);
|
||||
const [contextPosition, setContextPosition] = React.useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledLiveEditor>
|
||||
<JSONGraph
|
||||
ref={setJCRef}
|
||||
json={json}
|
||||
style={style}
|
||||
onNodeClick={handleNodeClick}
|
||||
onLayoutChange={handleLayoutChange}
|
||||
layout={layout}
|
||||
/>
|
||||
<StyledLiveEditor
|
||||
onContextMenuCapture={e => {
|
||||
e.preventDefault();
|
||||
setContextOpened(true);
|
||||
setContextPosition({ x: e.pageX, y: e.pageY });
|
||||
}}
|
||||
onClick={() => setContextOpened(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: contextPosition.y,
|
||||
left: contextPosition.x,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<Menu opened={false} shadow="sm">
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item>
|
||||
<Text size="xs">Download as Image</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Text size="xs">Zoom to Fit</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Text size="xs">Rotate</Text>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</div>
|
||||
|
||||
<View />
|
||||
</StyledLiveEditor>
|
||||
);
|
||||
};
|
||||
|
||||
export default LiveEditor;
|
||||
// import React from "react";
|
||||
// import styled from "styled-components";
|
||||
// import { Graph } from "src/components/Graph";
|
||||
|
||||
// const StyledLiveEditor = styled.div`
|
||||
// position: relative;
|
||||
// height: 100%;
|
||||
// `;
|
||||
|
||||
// const LiveEditor: React.FC = () => {
|
||||
// return (
|
||||
// <StyledLiveEditor>
|
||||
// <Graph />
|
||||
// </StyledLiveEditor>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default LiveEditor;
|
||||
|
@ -28,7 +28,7 @@ import { AiOutlineLink } from "react-icons/ai";
|
||||
import { FaTrash } from "react-icons/fa";
|
||||
import { MdFileOpen } from "react-icons/md";
|
||||
import { VscAdd, VscEdit, VscLock, VscUnlock } from "react-icons/vsc";
|
||||
import { FileFormat } from "src/enums/file";
|
||||
import { FileFormat } from "src/enums/file.enum";
|
||||
import { documentSvc } from "src/services/document.service";
|
||||
import useFile, { File } from "src/store/useFile";
|
||||
import useUser from "src/store/useUser";
|
||||
|
@ -3,12 +3,12 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { VscLinkExternal } from "react-icons/vsc";
|
||||
import useJsonQuery from "src/hooks/useJsonQuery";
|
||||
import useStored from "src/store/useStored";
|
||||
import useConfig from "src/store/useConfig";
|
||||
|
||||
export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
const { updateJson } = useJsonQuery();
|
||||
const [query, setQuery] = React.useState("");
|
||||
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
|
||||
const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
|
||||
|
||||
const onApply = () => {
|
||||
updateJson(query);
|
||||
@ -27,7 +27,7 @@ export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
</Text>
|
||||
<Editor
|
||||
value={query ?? ""}
|
||||
theme={lightmode}
|
||||
theme={darkmodeEnabled}
|
||||
onChange={e => setQuery(e!)}
|
||||
height={300}
|
||||
language="markdown"
|
||||
|
@ -6,10 +6,10 @@ import vsDark from "prism-react-renderer/themes/vsDark";
|
||||
import vsLight from "prism-react-renderer/themes/vsLight";
|
||||
import { VscLock } from "react-icons/vsc";
|
||||
import { isIframe } from "src/lib/utils/widget";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useFile from "src/store/useFile";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useModal from "src/store/useModal";
|
||||
import useStored from "src/store/useStored";
|
||||
import useUser from "src/store/useUser";
|
||||
|
||||
const dataToString = (data: any) => {
|
||||
@ -49,7 +49,7 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
const isPremium = useUser(state => state.premium);
|
||||
const editContents = useFile(state => state.editContents);
|
||||
const setVisible = useModal(state => state.setVisible);
|
||||
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
|
||||
const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
|
||||
const nodeData = useGraph(state => dataToString(state.selectedNode?.text));
|
||||
const path = useGraph(state => state.selectedNode?.path);
|
||||
const isParent = useGraph(state => state.selectedNode?.data?.isParent);
|
||||
@ -90,7 +90,7 @@ export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
</Text>
|
||||
{editMode ? (
|
||||
<Editor
|
||||
theme={lightmode}
|
||||
theme={darkmodeEnabled}
|
||||
defaultValue={nodeData}
|
||||
onChange={e => setValue(e!)}
|
||||
height={200}
|
||||
|
@ -3,9 +3,9 @@ import { Stack, Modal, Button, ModalProps, Text, Anchor, Group } from "@mantine/
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { VscLock } from "react-icons/vsc";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useFile from "src/store/useFile";
|
||||
import useModal from "src/store/useModal";
|
||||
import useStored from "src/store/useStored";
|
||||
import useUser from "src/store/useUser";
|
||||
|
||||
export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
@ -13,7 +13,7 @@ export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
const showPremiumModal = useModal(state => state.setVisible("premium"));
|
||||
const setJsonSchema = useFile(state => state.setJsonSchema);
|
||||
const [schema, setSchema] = React.useState("");
|
||||
const lightmode = useStored(state => (state.lightmode ? "light" : "vs-dark"));
|
||||
const darkmodeEnabled = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
|
||||
|
||||
const onApply = () => {
|
||||
if (!isPremium) return showPremiumModal(true);
|
||||
@ -47,7 +47,7 @@ export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
</Text>
|
||||
<Editor
|
||||
value={schema ?? ""}
|
||||
theme={lightmode}
|
||||
theme={darkmodeEnabled}
|
||||
onChange={e => setSchema(e!)}
|
||||
height={300}
|
||||
language="json"
|
||||
|
@ -1,93 +0,0 @@
|
||||
import React from "react";
|
||||
import { Modal, Group, Switch, Stack, ModalProps } from "@mantine/core";
|
||||
// import { VscLock } from "react-icons/vsc";
|
||||
// import useToggleHide from "src/hooks/useToggleHide";
|
||||
// import useGraph from "src/store/useGraph";
|
||||
// import useModal from "src/store/useModal";
|
||||
// import useUser from "src/store/useUser";
|
||||
import useStored from "src/store/useStored";
|
||||
|
||||
export const SettingsModal: React.FC<ModalProps> = ({ opened, onClose }) => {
|
||||
// const { validateHiddenNodes } = useToggleHide();
|
||||
// const setVisible = useModal(state => state.setVisible);
|
||||
// const toggleCollapseAll = useGraph(state => state.toggleCollapseAll);
|
||||
// const collapseAll = useGraph(state => state.collapseAll);
|
||||
// const isPremium = useUser(state => state.premium);
|
||||
const setLightTheme = useStored(state => state.setLightTheme);
|
||||
const toggleHideCollapse = useStored(state => state.toggleHideCollapse);
|
||||
const toggleChildrenCount = useStored(state => state.toggleChildrenCount);
|
||||
const toggleImagePreview = useStored(state => state.toggleImagePreview);
|
||||
const toggleGestures = useStored(state => state.toggleGestures);
|
||||
|
||||
const hideCollapse = useStored(state => state.hideCollapse);
|
||||
const childrenCount = useStored(state => state.childrenCount);
|
||||
const imagePreview = useStored(state => state.imagePreview);
|
||||
const lightmode = useStored(state => state.lightmode);
|
||||
const gesturesEnabled = useStored(state => state.gesturesEnabled);
|
||||
|
||||
return (
|
||||
<Modal title="Settings" opened={opened} onClose={onClose} centered>
|
||||
<Group py="sm">
|
||||
<Stack>
|
||||
<Switch
|
||||
label="Enable Trackpad Gestures"
|
||||
size="md"
|
||||
color="teal"
|
||||
onChange={e => toggleGestures(e.currentTarget.checked)}
|
||||
checked={gesturesEnabled}
|
||||
/>
|
||||
<Switch
|
||||
label="Live Image Preview"
|
||||
size="md"
|
||||
color="teal"
|
||||
onChange={e => toggleImagePreview(e.currentTarget.checked)}
|
||||
checked={imagePreview}
|
||||
/>
|
||||
<Switch
|
||||
label="Display Collapse/Expand Button"
|
||||
size="md"
|
||||
color="teal"
|
||||
onChange={e => toggleHideCollapse(e.currentTarget.checked)}
|
||||
checked={hideCollapse}
|
||||
/>
|
||||
<Switch
|
||||
label="Display Children Count"
|
||||
size="md"
|
||||
color="teal"
|
||||
onChange={e => toggleChildrenCount(e.currentTarget.checked)}
|
||||
checked={childrenCount}
|
||||
/>
|
||||
{/* <Switch
|
||||
label={
|
||||
<Flex align="center" gap="xs">
|
||||
Collapse All by Default
|
||||
<Badge size="xs" color="violet" variant="outline" radius="sm">
|
||||
35% Faster
|
||||
</Badge>
|
||||
</Flex>
|
||||
}
|
||||
size="md"
|
||||
color="violet"
|
||||
onChange={e => {
|
||||
if (isPremium) {
|
||||
toggleCollapseAll(e.currentTarget.checked);
|
||||
return validateHiddenNodes();
|
||||
}
|
||||
setVisible("premium")(true);
|
||||
onClose();
|
||||
}}
|
||||
checked={collapseAll}
|
||||
offLabel={isPremium ? null : <VscLock size="12" />}
|
||||
/> */}
|
||||
<Switch
|
||||
label="Light Theme"
|
||||
size="md"
|
||||
color="teal"
|
||||
onChange={e => setLightTheme(e.currentTarget.checked)}
|
||||
checked={lightmode}
|
||||
/>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -4,7 +4,6 @@ export { DownloadModal } from "./DownloadModal";
|
||||
export { ImportModal } from "./ImportModal";
|
||||
export { AccountModal } from "./AccountModal";
|
||||
export { NodeModal } from "./NodeModal";
|
||||
export { SettingsModal } from "./SettingsModal";
|
||||
export { ShareModal } from "./ShareModal";
|
||||
export { LoginModal } from "./LoginModal";
|
||||
export { PremiumModal } from "./PremiumModal";
|
||||
@ -22,7 +21,6 @@ type Modal =
|
||||
| "import"
|
||||
| "account"
|
||||
| "node"
|
||||
| "settings"
|
||||
| "share"
|
||||
| "login"
|
||||
| "premium"
|
||||
|
@ -1,3 +1,4 @@
|
||||
// eslint-disable
|
||||
export enum FileFormat {
|
||||
"JSON" = "json",
|
||||
"YAML" = "yaml",
|
4
src/enums/viewMode.enum.ts
Normal file
4
src/enums/viewMode.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum ViewMode {
|
||||
Graph = "graph",
|
||||
Tree = "tree",
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import React from "react";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { searchQuery, cleanupHighlight, highlightMatchedNodes } from "src/lib/utils/graph/search";
|
||||
import useJC from "src/store/useJC";
|
||||
import useGraph from "src/store/useGraph";
|
||||
|
||||
export const useFocusNode = () => {
|
||||
const viewPort = useJC(state => state.graphRef?.viewPort);
|
||||
const viewPort = useGraph(state => state.viewPort);
|
||||
const [selectedNode, setSelectedNode] = React.useState(0);
|
||||
const [nodeCount, setNodeCount] = React.useState(0);
|
||||
const [value, setValue] = React.useState("");
|
||||
|
@ -4,7 +4,7 @@ import { MantineProvider, MantineThemeOverride } from "@mantine/core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { monaSans } from "src/constants/fonts";
|
||||
import { lightTheme, darkTheme } from "src/constants/theme";
|
||||
import useStored from "src/store/useStored";
|
||||
import useConfig from "src/store/useConfig";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@ -52,13 +52,13 @@ const mantineTheme: MantineThemeOverride = {
|
||||
};
|
||||
|
||||
export const EditorWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const lightmode = useStored(state => state.lightmode);
|
||||
const darkmodeEnabled = useConfig(state => state.darkmodeEnabled);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
|
||||
<ThemeProvider theme={darkmodeEnabled ? darkTheme : lightTheme}>
|
||||
<MantineProvider
|
||||
theme={{
|
||||
colorScheme: lightmode ? "light" : "dark",
|
||||
colorScheme: darkmodeEnabled ? "dark" : "light",
|
||||
...mantineTheme,
|
||||
}}
|
||||
withCSSVariables
|
||||
|
@ -11,7 +11,6 @@ const modalComponents: ModalComponent[] = [
|
||||
{ key: "import", component: Modals.ImportModal },
|
||||
{ key: "clear", component: Modals.ClearModal },
|
||||
{ key: "download", component: Modals.DownloadModal },
|
||||
{ key: "settings", component: Modals.SettingsModal },
|
||||
{ key: "cloud", component: Modals.CloudModal },
|
||||
{ key: "account", component: Modals.AccountModal },
|
||||
{ key: "premium", component: Modals.PremiumModal },
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { firaMono } from "src/constants/fonts";
|
||||
import useConfig from "src/store/useConfig";
|
||||
import useGraph from "src/store/useGraph";
|
||||
import useStored from "src/store/useStored";
|
||||
|
||||
type Text = string | [string, string][];
|
||||
type Size = { width: number; height: number };
|
||||
@ -54,8 +54,8 @@ setInterval(() => sizeCache.clear(), 120_000);
|
||||
|
||||
export const calculateNodeSize = (text: Text, isParent = false) => {
|
||||
const { foldNodes } = useGraph.getState();
|
||||
const { imagePreview } = useStored.getState();
|
||||
const isImage = isContentImage(text) && imagePreview;
|
||||
const { imagePreviewEnabled } = useConfig.getState();
|
||||
const isImage = isContentImage(text) && imagePreviewEnabled;
|
||||
|
||||
const cacheKey = [text, isParent, foldNodes].toString();
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { csv2json, json2csv } from "json-2-csv";
|
||||
import { parse } from "jsonc-parser";
|
||||
import jxon from "jxon";
|
||||
import toml from "toml";
|
||||
import { FileFormat } from "src/enums/file";
|
||||
import { FileFormat } from "src/enums/file.enum";
|
||||
|
||||
const keyExists = (obj: object, key: string) => {
|
||||
if (!obj || (typeof obj !== "object" && !Array.isArray(obj))) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PostgrestSingleResponse } from "@supabase/supabase-js";
|
||||
import toast from "react-hot-toast";
|
||||
import { FileFormat } from "src/enums/file";
|
||||
import { FileFormat } from "src/enums/file.enum";
|
||||
import { supabase } from "src/lib/api/supabase";
|
||||
import { File } from "src/store/useFile";
|
||||
import useUser from "src/store/useUser";
|
||||
|
50
src/store/useConfig.ts
Normal file
50
src/store/useConfig.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { ViewMode } from "src/enums/viewMode.enum";
|
||||
import useGraph from "./useGraph";
|
||||
|
||||
const initialStates = {
|
||||
darkmodeEnabled: false,
|
||||
collapseButtonVisible: true,
|
||||
childrenCountVisible: true,
|
||||
imagePreviewEnabled: true,
|
||||
liveTransformEnabled: true,
|
||||
gesturesEnabled: false,
|
||||
rulersEnabled: true,
|
||||
viewMode: ViewMode.Graph,
|
||||
};
|
||||
|
||||
export interface ConfigActions {
|
||||
toggleDarkMode: (value: boolean) => void;
|
||||
toggleCollapseButton: (value: boolean) => void;
|
||||
toggleChildrenCount: (value: boolean) => void;
|
||||
toggleImagePreview: (value: boolean) => void;
|
||||
toggleLiveTransform: (value: boolean) => void;
|
||||
toggleGestures: (value: boolean) => void;
|
||||
toggleRulers: (value: boolean) => void;
|
||||
setViewMode: (value: ViewMode) => void;
|
||||
}
|
||||
|
||||
const useConfig = create(
|
||||
persist<typeof initialStates & ConfigActions>(
|
||||
set => ({
|
||||
...initialStates,
|
||||
toggleRulers: rulersEnabled => set({ rulersEnabled }),
|
||||
toggleGestures: gesturesEnabled => set({ gesturesEnabled }),
|
||||
toggleLiveTransform: liveTransformEnabled => set({ liveTransformEnabled }),
|
||||
toggleDarkMode: darkmodeEnabled => set({ darkmodeEnabled }),
|
||||
toggleCollapseButton: collapseButtonVisible => set({ collapseButtonVisible }),
|
||||
toggleChildrenCount: childrenCountVisible => set({ childrenCountVisible }),
|
||||
toggleImagePreview: imagePreviewEnabled => {
|
||||
set({ imagePreviewEnabled });
|
||||
useGraph.getState().setGraph();
|
||||
},
|
||||
setViewMode: viewMode => set({ viewMode }),
|
||||
}),
|
||||
{
|
||||
name: "config",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useConfig;
|
@ -5,13 +5,13 @@ import ReactGA from "react-ga4";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { create } from "zustand";
|
||||
import { defaultJson } from "src/constants/data";
|
||||
import { FileFormat } from "src/enums/file";
|
||||
import { FileFormat } from "src/enums/file.enum";
|
||||
import { contentToJson, jsonToContent } from "src/lib/utils/json/jsonAdapter";
|
||||
import { isIframe } from "src/lib/utils/widget";
|
||||
import { documentSvc } from "src/services/document.service";
|
||||
import useConfig from "./useConfig";
|
||||
import useGraph from "./useGraph";
|
||||
import useJson from "./useJson";
|
||||
import useStored from "./useStored";
|
||||
import useUser from "./useUser";
|
||||
|
||||
type SetContents = {
|
||||
@ -125,7 +125,7 @@ const useFile = create<FileStates & JsonActions>()((set, get) => ({
|
||||
const isFetchURL = window.location.href.includes("?");
|
||||
const json = await contentToJson(get().contents, get().format);
|
||||
|
||||
if (!useStored.getState().liveTransform && skipUpdate) return;
|
||||
if (!useConfig.getState().liveTransformEnabled && skipUpdate) return;
|
||||
|
||||
if (get().hasChanges && contents && contents.length < 80_000 && !isIframe() && !isFetchURL) {
|
||||
sessionStorage.setItem("content", contents);
|
||||
|
@ -61,6 +61,7 @@ interface GraphActions {
|
||||
zoomOut: () => void;
|
||||
centerView: () => void;
|
||||
clearGraph: () => void;
|
||||
setZoomFactor: (zoomFactor: number) => void;
|
||||
}
|
||||
|
||||
const useGraph = create<Graph & GraphActions>((set, get) => ({
|
||||
@ -193,6 +194,10 @@ const useGraph = create<Graph & GraphActions>((set, get) => ({
|
||||
elementExtraMarginForZoom: 100,
|
||||
});
|
||||
},
|
||||
setZoomFactor: zoomFactor => {
|
||||
const viewPort = get().viewPort;
|
||||
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, zoomFactor);
|
||||
},
|
||||
zoomIn: () => {
|
||||
const viewPort = get().viewPort;
|
||||
viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, viewPort.zoomFactor + 0.1);
|
||||
|
@ -1,19 +0,0 @@
|
||||
import { GraphRef } from "jsongraph-react";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface JCActions {
|
||||
setJCRef: (graphRef: GraphRef) => void;
|
||||
}
|
||||
|
||||
const initialStates = {
|
||||
graphRef: null as unknown as GraphRef,
|
||||
};
|
||||
|
||||
export type JCStates = typeof initialStates;
|
||||
|
||||
const useJC = create<JCStates & JCActions>()(set => ({
|
||||
...initialStates,
|
||||
setJCRef: graphRef => set({ graphRef }),
|
||||
}));
|
||||
|
||||
export default useJC;
|
@ -18,7 +18,6 @@ const initialStates: ModalState = {
|
||||
import: false,
|
||||
account: false,
|
||||
node: false,
|
||||
settings: false,
|
||||
share: false,
|
||||
login: false,
|
||||
premium: false,
|
||||
|
@ -1,46 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import useGraph from "./useGraph";
|
||||
|
||||
const initialStates = {
|
||||
lightmode: false,
|
||||
hideCollapse: true,
|
||||
childrenCount: true,
|
||||
imagePreview: true,
|
||||
liveTransform: true,
|
||||
gesturesEnabled: false,
|
||||
};
|
||||
|
||||
export interface ConfigActions {
|
||||
setLightTheme: (theme: boolean) => void;
|
||||
toggleHideCollapse: (value: boolean) => void;
|
||||
toggleChildrenCount: (value: boolean) => void;
|
||||
toggleImagePreview: (value: boolean) => void;
|
||||
toggleLiveTransform: (value: boolean) => void;
|
||||
toggleGestures: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const useStored = create(
|
||||
persist<typeof initialStates & ConfigActions>(
|
||||
set => ({
|
||||
...initialStates,
|
||||
toggleGestures: gesturesEnabled => set({ gesturesEnabled }),
|
||||
toggleLiveTransform: liveTransform => set({ liveTransform }),
|
||||
setLightTheme: (value: boolean) =>
|
||||
set({
|
||||
lightmode: value,
|
||||
}),
|
||||
toggleHideCollapse: (value: boolean) => set({ hideCollapse: value }),
|
||||
toggleChildrenCount: (value: boolean) => set({ childrenCount: value }),
|
||||
toggleImagePreview: (value: boolean) => {
|
||||
set({ imagePreview: value });
|
||||
useGraph.getState().setGraph();
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "config",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useStored;
|
Loading…
x
Reference in New Issue
Block a user