diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a5bc75b59..0f93bd38c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -12,7 +12,7 @@ publish = false tauri-build = { version = "1.4.0", features = [] } [dependencies] -tauri = { version = "1.4.1", features = [ "dialog-open", "fs-read-file", "devtools", "dialog-save", "fs-write-file", "shell-open", "window-set-always-on-top", "window-set-title", "window-show"] } +tauri = { version = "1.4.1", features = [ "path-all", "dialog-open", "fs-read-file", "devtools", "dialog-save", "fs-write-file", "shell-open", "window-set-always-on-top", "window-set-title", "window-show"] } tauri-plugin-localhost = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } portpicker = "0.1" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c97216e74..784215613 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -27,6 +27,9 @@ "dialog": { "open": true, "save": true + }, + "path": { + "all": true } }, "bundle": { diff --git a/src/adapter/base.tsx b/src/adapter/base.tsx index ba2f946a2..9e54c885d 100644 --- a/src/adapter/base.tsx +++ b/src/adapter/base.tsx @@ -1,5 +1,10 @@ import { Result } from "~/typings/utilities"; +export interface OpenedFile { + name: string; + content: string; +} + export interface SurrealistAdapter { /** @@ -87,6 +92,6 @@ export interface SurrealistAdapter { title: string, filters: any, multiple: boolean - ): Promise; + ): Promise; } diff --git a/src/adapter/browser.tsx b/src/adapter/browser.tsx index 8318c6ffd..4abc3acbf 100644 --- a/src/adapter/browser.tsx +++ b/src/adapter/browser.tsx @@ -1,5 +1,5 @@ import { Result } from "~/typings/utilities"; -import { SurrealistAdapter } from "./base"; +import { OpenedFile, SurrealistAdapter } from "./base"; /** * Surrealist adapter for running as web app @@ -71,7 +71,7 @@ export class BrowserAdapter implements SurrealistAdapter { return true; } - public async openFile(): Promise { + public async openFile(): Promise { const el = document.createElement('input'); el.type = 'file'; @@ -81,13 +81,15 @@ export class BrowserAdapter implements SurrealistAdapter { return new Promise((resolve, reject) => { el.addEventListener('change', async () => { - const text = await el.files?.[0]?.text(); + const files = [...(el.files ?? [])]; + const tasks = files.map(async (file) => ({ + name: file.name, + content: await file.text(), + })); - if (typeof text == 'string') { - resolve(text); - } else { - resolve(null); - } + const results = await Promise.all(tasks); + + resolve(results); }); el.addEventListener('error', async () => { diff --git a/src/adapter/desktop.tsx b/src/adapter/desktop.tsx index fa5b14cf6..9a645eafb 100644 --- a/src/adapter/desktop.tsx +++ b/src/adapter/desktop.tsx @@ -2,11 +2,12 @@ import { invoke } from "@tauri-apps/api/tauri"; import { appWindow } from "@tauri-apps/api/window"; import { open as openURL } from "@tauri-apps/api/shell"; import { save, open } from "@tauri-apps/api/dialog"; +import { basename } from "@tauri-apps/api/path"; import { listen } from "@tauri-apps/api/event"; import { Stack, Text } from "@mantine/core"; import { showNotification } from "@mantine/notifications"; import { store } from "~/store"; -import { SurrealistAdapter } from "./base"; +import { OpenedFile, SurrealistAdapter } from "./base"; import { printLog } from "~/util/helpers"; import { readTextFile, writeBinaryFile, writeTextFile } from "@tauri-apps/api/fs"; import { Result } from "~/typings/utilities"; @@ -115,18 +116,25 @@ export class DesktopAdapter implements SurrealistAdapter { title: string, filters: any, multiple: boolean - ): Promise { - const url = await open({ + ): Promise { + const result = await open({ title, filters, multiple }); - if (!url) { - return null; - } - - return readTextFile(url as string); + const urls = typeof result === "string" + ? [result] + : result === null + ? [] + : result; + + const tasks = urls.map(async (url) => ({ + name: await basename(url), + content: await readTextFile(url) + })); + + return Promise.all(tasks); } private initDatabaseEvents() { diff --git a/src/components/Exporter/index.tsx b/src/components/Exporter/index.tsx index f96e8c3a4..d575505cc 100644 --- a/src/components/Exporter/index.tsx +++ b/src/components/Exporter/index.tsx @@ -59,6 +59,7 @@ export function Exporter() { color={isLight ? "light.0" : "dark.4"} title="Export database to file" onClick={openExporter} + loading={isExporting} disabled={!isOnline} > diff --git a/src/components/Importer/index.tsx b/src/components/Importer/index.tsx new file mode 100644 index 000000000..47cf99698 --- /dev/null +++ b/src/components/Importer/index.tsx @@ -0,0 +1,148 @@ +import { Button, Group, Modal, Paper } from "@mantine/core"; +import { SURQL_FILTERS } from "~/constants"; +import { useIsConnected } from "~/hooks/connection"; +import { useStable } from "~/hooks/stable"; +import { useIsLight } from "~/hooks/theme"; +import { useRef, useState } from "react"; +import { Icon } from "../Icon"; +import { mdiFileDocument, mdiUpload } from "@mdi/js"; +import { adapter } from "~/adapter"; +import { showNotification } from "@mantine/notifications"; +import { showError } from "~/util/helpers"; +import { ModalTitle } from "../ModalTitle"; +import { useDisclosure } from "@mantine/hooks"; +import { Spacer } from "../Spacer"; +import { Text } from "@mantine/core"; +import { fetchDatabaseSchema } from "~/util/schema"; +import { getActiveSurreal } from "~/util/connection"; +import { OpenedFile } from "~/adapter/base"; + +export function Importer() { + const isLight = useIsLight(); + const isOnline = useIsConnected(); + const [showConfirm, showConfirmHandle] = useDisclosure(); + const [isImporting, setIsImporting] = useState(false); + + const importFile = useRef(null); + + const startImport = useStable(async () => { + try { + const [file] = await adapter.openFile( + 'Import query file', + SURQL_FILTERS, + false + ); + + if (!file) { + return; + } + + importFile.current = file; + showConfirmHandle.open(); + } finally { + setIsImporting(false); + } + }); + + const confirmImport = useStable(async () => { + try { + setIsImporting(true); + + await getActiveSurreal().query(importFile.current!.content); + + showNotification({ + title: 'Import successful', + message: 'The database was successfully imported', + }); + + fetchDatabaseSchema(); + } catch(err: any) { + console.error(err); + + showError("Import failed", "There was an error importing the database"); + } finally { + setIsImporting(false); + showConfirmHandle.close(); + } + }); + + return ( + <> + + + Import database} + > + + + + + {importFile.current?.name} + + + + + + Are you sure you want to import the selected file? + + + + While existing data will be preserved, it may be overwritten by the imported data. + + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/Toolbar/index.tsx b/src/components/Toolbar/index.tsx index b52f6bc16..767145675 100644 --- a/src/components/Toolbar/index.tsx +++ b/src/components/Toolbar/index.tsx @@ -18,6 +18,7 @@ import { useTabsList } from "~/hooks/environment"; import { ViewTab } from "../ViewTab"; import { Exporter } from "../Exporter"; import { updateSession, setShowQueryListing, setQueryListingMode } from "~/stores/config"; +import { Importer } from "../Importer"; export interface ToolbarProps { viewMode: ViewMode; @@ -116,6 +117,8 @@ export function Toolbar(props: ToolbarProps) { )} + + diff --git a/src/views/query/QueryPane/index.tsx b/src/views/query/QueryPane/index.tsx index 28a19457a..fe32b89ab 100644 --- a/src/views/query/QueryPane/index.tsx +++ b/src/views/query/QueryPane/index.tsx @@ -1,6 +1,6 @@ import classes from './style.module.scss'; import { editor } from "monaco-editor"; -import { mdiClose, mdiDatabase, mdiPlusBoxMultiple, mdiUpload } from "@mdi/js"; +import { mdiClose, mdiDatabase, mdiFileDocument, mdiPlusBoxMultiple } from "@mdi/js"; import { useStable } from "~/hooks/stable"; import { useActiveSession } from "~/hooks/environment"; import { store } from "~/store"; @@ -45,10 +45,10 @@ export function QueryPane() { }); const handleUpload = useStable(async () => { - const query = await adapter.openFile('Load query from file', SURQL_FILTERS, false); + const [file] = await adapter.openFile('Load query from file', SURQL_FILTERS, false); - if (typeof query == 'string') { - setQueryForced(query); + if (file) { + setQueryForced(file.content); } }); @@ -89,7 +89,7 @@ export function QueryPane() { - + }