Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/features/editor/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { VscCheck, VscError, VscRunAll, VscSync, VscSyncIgnored } from "react-ic
import { formats } from "../../enums/file.enum";
import useConfig from "../../store/useConfig";
import useFile from "../../store/useFile";
import useWatchMode from "../../store/useWatchMode";
import useGraph from "./views/GraphView/stores/useGraph";

const StyledBottomBar = styled.div`
Expand Down Expand Up @@ -84,6 +85,8 @@ export const BottomBar = () => {
const fullscreen = useGraph(state => state.fullscreen);
const setFormat = useFile(state => state.setFormat);
const currentFormat = useFile(state => state.format);
const toggleWatchMode = useWatchMode(state => state.toggleWatchMode);
const isWatching = useWatchMode(state => state.isWatching);

const toggleEditor = () => {
toggleFullscreen(!fullscreen);
Expand Down Expand Up @@ -123,6 +126,7 @@ export const BottomBar = () => {
)}
</StyledBottomBarItem>
<StyledBottomBarItem
disabled={isWatching}
onClick={() => {
toggleLiveTransform(!liveTransformEnabled);
gaEvent("toggle_live_transform");
Expand All @@ -131,18 +135,24 @@ export const BottomBar = () => {
{liveTransformEnabled ? <VscSync /> : <VscSyncIgnored />}
<Text fz="xs">Live Transform</Text>
</StyledBottomBarItem>
{!liveTransformEnabled && (
{!liveTransformEnabled && !isWatching && (
<StyledBottomBarItem onClick={() => setContents({})} disabled={!!error}>
<VscRunAll />
Click to Transform
</StyledBottomBarItem>
)}
{isWatching && (
<StyledBottomBarItem onClick={() => toggleWatchMode(false)}>
<VscSyncIgnored />
<Text fz="xs">Stop Watching</Text>
</StyledBottomBarItem>
)}
</StyledLeft>

<StyledRight>
<Menu offset={8}>
<Menu.Target>
<StyledBottomBarItem>
<StyledBottomBarItem disabled={isWatching}>
<Flex align="center" gap={2}>
<MdArrowUpward />
<Text size="xs">{currentFormat?.toUpperCase()}</Text>
Expand Down
10 changes: 9 additions & 1 deletion src/features/editor/TextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from "styled-components";
import Editor, { type EditorProps, loader, type OnMount, useMonaco } from "@monaco-editor/react";
import useConfig from "../../store/useConfig";
import useFile from "../../store/useFile";
import useWatchMode from "../../store/useWatchMode";

loader.config({
paths: {
Expand All @@ -19,6 +20,9 @@ const editorOptions: EditorProps["options"] = {
stickyScroll: { enabled: false },
scrollBeyondLastLine: false,
placeholder: "Start typing...",
readOnlyMessage: {
value: "You can't edit the file while watching mode is enabled ",
},
};

const TextEditor = () => {
Expand All @@ -30,6 +34,7 @@ const TextEditor = () => {
const getHasChanges = useFile(state => state.getHasChanges);
const theme = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));
const fileType = useFile(state => state.format);
const isWatching = useWatchMode(state => state.isWatching);

React.useEffect(() => {
monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
Expand Down Expand Up @@ -82,7 +87,10 @@ const TextEditor = () => {
language={fileType}
theme={theme}
value={contents}
options={editorOptions}
options={{
...editorOptions,
readOnly: isWatching,
}}
onMount={handleMount}
onValidate={errors => setError(errors[0]?.message || "")}
onChange={contents => setContents({ contents, skipUpdate: true })}
Expand Down
80 changes: 79 additions & 1 deletion src/features/modals/ImportModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import React from "react";
import type { ModalProps } from "@mantine/core";
import { Modal, Group, Button, TextInput, Stack, Paper, Text } from "@mantine/core";
import { Modal, Group, Button, TextInput, Stack, Paper, Text, Tooltip } from "@mantine/core";
import { Dropzone } from "@mantine/dropzone";
import { event as gaEvent } from "nextjs-google-analytics";
import toast from "react-hot-toast";
import { AiOutlineUpload } from "react-icons/ai";
import type { FileFormat } from "../../../enums/file.enum";
import useFile from "../../../store/useFile";
import useWatchMode from "../../../store/useWatchMode";

export const ImportModal = ({ opened, onClose }: ModalProps) => {
const [url, setURL] = React.useState("");
const [file, setFile] = React.useState<File | null>(null);

const availableFilePicker = React.useMemo(() => {
if (typeof window === "undefined") return false;
return "showOpenFilePicker" in window;
}, []);

const toggleWatchMode = useWatchMode(state => state.toggleWatchMode);
const setWatcherInterval = useWatchMode(state => state.setWatcherInterval);
const setContents = useFile(state => state.setContents);
const setFormat = useFile(state => state.setFormat);

Expand All @@ -26,6 +34,7 @@ export const ImportModal = ({ opened, onClose }: ModalProps) => {
.then(res => res.json())
.then(json => {
setContents({ contents: JSON.stringify(json, null, 2) });
toggleWatchMode(false);
onClose();
})
.catch(() => toast.error("Failed to fetch JSON!"))
Expand All @@ -35,8 +44,10 @@ export const ImportModal = ({ opened, onClose }: ModalProps) => {
const format = file.name.substring(lastIndex + 1);
setFormat(format as FileFormat);

console.log(file);
file.text().then(text => {
setContents({ contents: text });
toggleWatchMode(false);
setFile(null);
setURL("");
onClose();
Expand All @@ -46,11 +57,77 @@ export const ImportModal = ({ opened, onClose }: ModalProps) => {
}
};

const enableWatcher = React.useCallback(async () => {
if (!availableFilePicker) return;

const [fileHandle] = await window.showOpenFilePicker({
types: [
{
description: "Any file",
accept: {
"application/json": [".json"],
"application/x-yaml": [".yaml", ".yml"],
"text/csv": [".csv"],
"application/xml": [".xml"],
"application/toml": [".toml"],
},
},
],
}).catch(() => []);

if (!fileHandle) return;

const file = await fileHandle.getFile();
let lastModified = file.lastModified;
const text = await file.text();
const lastIndex = file.name.lastIndexOf(".");
const format = file.name.substring(lastIndex + 1);
setFormat(format as FileFormat);
setContents({ contents: text });
toggleWatchMode(true);
const interval = setInterval(async () => {
const newFile = await fileHandle.getFile();
if (newFile.lastModified !== lastModified) {
lastModified = newFile.lastModified;
const text = await newFile.text();
setContents({ contents: text });
toast.success("File updated!");
}
}, 3_000);
setWatcherInterval(interval);
setFile(null);
setURL("");
onClose();
}, [availableFilePicker, onClose, setContents, setFormat, setWatcherInterval, toggleWatchMode]);

const WatcherButton = () => {
if (!availableFilePicker)
return (
<Tooltip
label="Not suported in this browser"
fz="xs"
ta="center"
maw="200"
withArrow
openDelay={500}
>
<Button disabled={true}>Watcher</Button>
</Tooltip>
);

return (
<Button onClick={enableWatcher} disabled={!!(file || url)}>
Watcher
</Button>
);
};

return (
<Modal
title="Import File"
opened={opened}
onClose={() => {
toggleWatchMode(false);
setFile(null);
setURL("");
onClose();
Expand Down Expand Up @@ -90,6 +167,7 @@ export const ImportModal = ({ opened, onClose }: ModalProps) => {
</Paper>
</Stack>
<Group justify="right">
<WatcherButton />
<Button onClick={handleImportFile} disabled={!(file || url)}>
Import
</Button>
Expand Down
34 changes: 34 additions & 0 deletions src/store/useWatchMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { create } from "zustand";

interface WatchModeState {
isWatching: boolean;
interval: NodeJS.Timeout | null;
setWatcherInterval: (interval: NodeJS.Timeout) => void;
toggleWatchMode: (isWatching?: boolean) => boolean;
getIsWatching: () => boolean;
}

const useWatchMode = create<WatchModeState>((set, get) => ({
isWatching: false,
interval: null,

toggleWatchMode: isWatching => {
const newState = isWatching !== undefined ? isWatching : !get().isWatching;
if (!newState) {
const interval = get().interval;
if (interval) clearInterval(interval);
}
set({ isWatching: newState });
return newState;
},

getIsWatching: () => get().isWatching,

setWatcherInterval: interval => {
const oldInterval = get().interval;
if (oldInterval) clearInterval(oldInterval);
set({ interval });
},
}));

export default useWatchMode;
86 changes: 86 additions & 0 deletions src/types/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Global types for the File System Access API

interface FileSystemHandle {
readonly kind: "file" | "directory";
readonly name: string;
isSameEntry(other: FileSystemHandle): Promise<boolean>;
queryPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
requestPermission(descriptor?: FileSystemHandlePermissionDescriptor): Promise<PermissionState>;
}

interface FileSystemHandlePermissionDescriptor {
mode?: "read" | "readwrite";
}

interface FileSystemFileHandle extends FileSystemHandle {
readonly kind: "file";
getFile(): Promise<File>;
createWritable(options?: FileSystemCreateWritableOptions): Promise<FileSystemWritableFileStream>;
}

interface FileSystemDirectoryHandle extends FileSystemHandle {
readonly kind: "directory";
getFileHandle(name: string, options?: FileSystemGetFileOptions): Promise<FileSystemFileHandle>;
getDirectoryHandle(
name: string,
options?: FileSystemGetDirectoryOptions
): Promise<FileSystemDirectoryHandle>;
removeEntry(name: string, options?: FileSystemRemoveOptions): Promise<void>;
resolve(possibleDescendant: FileSystemHandle): Promise<string[] | null>;
entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
keys(): AsyncIterableIterator<string>;
values(): AsyncIterableIterator<FileSystemHandle>;
}

interface FileSystemCreateWritableOptions {
keepExistingData?: boolean;
}

interface FileSystemGetFileOptions {
create?: boolean;
}

interface FileSystemGetDirectoryOptions {
create?: boolean;
}

interface FileSystemRemoveOptions {
recursive?: boolean;
}

interface FilePickerAcceptType {
description?: string;
accept: Record<string, string[]>;
}

interface OpenFilePickerOptions {
multiple?: boolean;
excludeAcceptAllOption?: boolean;
id?: string;
types?: FilePickerAcceptType[];
}

interface SaveFilePickerOptions {
suggestedName?: string;
types?: FilePickerAcceptType[];
excludeAcceptAllOption?: boolean;
}

interface DirectoryPickerOptions {
id?: string;
mode?: "read" | "readwrite";
startIn?:
| FileSystemHandle
| "desktop"
| "documents"
| "downloads"
| "music"
| "pictures"
| "videos";
}

interface Window {
showOpenFilePicker(options?: OpenFilePickerOptions): Promise<FileSystemFileHandle[]>;
showSaveFilePicker(options?: SaveFilePickerOptions): Promise<FileSystemFileHandle>;
showDirectoryPicker(options?: DirectoryPickerOptions): Promise<FileSystemDirectoryHandle>;
}