diff --git a/packages/web/src/appobj/SavedFileAppObject.js b/packages/web/src/appobj/SavedFileAppObject.js index 7b6751679..63da73193 100644 --- a/packages/web/src/appobj/SavedFileAppObject.js +++ b/packages/web/src/appobj/SavedFileAppObject.js @@ -42,8 +42,7 @@ export function SavedSqlFileAppObject({ data, commonProps }) { onLoad={(data) => { newQuery({ title: file, - // @ts-ignore - initialScript: data, + initialData: data, }); }} /> @@ -60,14 +59,15 @@ export function SavedShellFileAppObject({ data, commonProps }) { format="text" icon="img shell" onLoad={(data) => { - openNewTab(setOpenedTabs, { - title: file, - icon: 'img shell', - tabComponent: 'ShellTab', - props: { - initialScript: data, + openNewTab( + setOpenedTabs, + { + title: file, + icon: 'img shell', + tabComponent: 'ShellTab', }, - }); + data + ); }} /> ); diff --git a/packages/web/src/modals/SaveFileModal.js b/packages/web/src/modals/SaveFileModal.js index 572ad3e97..5a08cec19 100644 --- a/packages/web/src/modals/SaveFileModal.js +++ b/packages/web/src/modals/SaveFileModal.js @@ -9,10 +9,10 @@ import ModalContent from './ModalContent'; import ModalFooter from './ModalFooter'; // import FormikForm from '../utility/FormikForm'; -export default function SaveFileModal({ getData, folder, format, modalState, name, onSave = undefined }) { +export default function SaveFileModal({ data, folder, format, modalState, name, onSave = undefined }) { const handleSubmit = async (values) => { const { name } = values; - await axios.post('files/save', { folder, file: name, data: getData(), format }); + await axios.post('files/save', { folder, file: name, data, format }); modalState.close(); if (onSave) onSave(name); }; diff --git a/packages/web/src/modals/SaveTabModal.js b/packages/web/src/modals/SaveTabModal.js new file mode 100644 index 000000000..0fc238709 --- /dev/null +++ b/packages/web/src/modals/SaveTabModal.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { changeTab } from '../utility/common'; +import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState'; +import SaveFileModal from './SaveFileModal'; + +export default function SaveTabModal({ data, folder, format, modalState, tabid }) { + const setOpenedTabs = useSetOpenedTabs(); + const openedTabs = useOpenedTabs(); + + const name = openedTabs.find((x) => x.tabid == tabid).title; + const onSave = (name) => changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, title: name })); + + return ( + + ); +} diff --git a/packages/web/src/query/useNewQuery.js b/packages/web/src/query/useNewQuery.js index a6e37ff6d..6aa8ea3b2 100644 --- a/packages/web/src/query/useNewQuery.js +++ b/packages/web/src/query/useNewQuery.js @@ -11,16 +11,20 @@ export default function useNewQuery() { const tooltip = `${connection.displayName || connection.server}\n${database}`; - return ({ title = undefined, ...props } = {}) => - openNewTab(setOpenedTabs, { - title: title || 'Query', - icon: 'img sql-file', - tooltip, - tabComponent: 'QueryTab', - props: { - ...props, - conid: connection._id, - database, + return ({ title = undefined, initialData = undefined, ...props } = {}) => + openNewTab( + setOpenedTabs, + { + title: title || 'Query', + icon: 'img sql-file', + tooltip, + tabComponent: 'QueryTab', + props: { + ...props, + conid: connection._id, + database, + }, }, - }); + initialData + ); } diff --git a/packages/web/src/tabs/QueryTab.js b/packages/web/src/tabs/QueryTab.js index 562993d45..5cbc7f292 100644 --- a/packages/web/src/tabs/QueryTab.js +++ b/packages/web/src/tabs/QueryTab.js @@ -1,11 +1,11 @@ import React from 'react'; -import ReactDOM from 'react-dom'; import _ from 'lodash'; +import ReactDOM from 'react-dom'; import axios from '../utility/axios'; -import { useConnectionInfo, getDbCore, getConnectionInfo, getSqlObjectInfo } from '../utility/metadataLoaders'; +import { useConnectionInfo } from '../utility/metadataLoaders'; import SqlEditor from '../sqleditor/SqlEditor'; -import { useUpdateDatabaseForTab, useSetOpenedTabs, useOpenedTabs } from '../utility/globalState'; +import { useUpdateDatabaseForTab, useSetOpenedTabs } from '../utility/globalState'; import QueryToolbar from '../query/QueryToolbar'; import SocketMessagesView from '../query/SocketMessagesView'; import { TabPage } from '../widgets/TabControl'; @@ -14,103 +14,38 @@ import { VerticalSplitter } from '../widgets/Splitter'; import keycodes from '../utility/keycodes'; import { changeTab } from '../utility/common'; import useSocket from '../utility/SocketProvider'; -import SaveFileModal from '../modals/SaveFileModal'; +import SaveTabModal from '../modals/SaveTabModal'; import useModalState from '../modals/useModalState'; import sqlFormatter from 'sql-formatter'; -import useExtensions from '../utility/useExtensions'; -import { driverBase, findEngineDriver } from 'dbgate-tools'; +import useEditorData from '../utility/useEditorData'; +import useSqlTemplate from '../utility/useSqlTemplate'; +import LoadingInfo from '../widgets/LoadingInfo'; -function useSqlTemplate(sqlTemplate, props) { - const [sql, setSql] = React.useState(''); - const extensions = useExtensions(); - - async function loadTemplate() { - if (sqlTemplate == 'CREATE TABLE') { - const tableInfo = await getDbCore(props, props.objectTypeField || 'tables'); - const connection = await getConnectionInfo(props); - const driver = findEngineDriver(connection, extensions) || driverBase; - const dmp = driver.createDumper(); - if (tableInfo) dmp.createTable(tableInfo); - setSql(dmp.s); - } - if (sqlTemplate == 'CREATE OBJECT') { - const objectInfo = await getSqlObjectInfo(props); - if (objectInfo) { - if (objectInfo.requiresFormat && objectInfo.createSql) setSql(sqlFormatter.format(objectInfo.createSql)); - else setSql(objectInfo.createSql); - } - } - if (sqlTemplate == 'EXECUTE PROCEDURE') { - const procedureInfo = await getSqlObjectInfo(props); - const connection = await getConnectionInfo(props); - - const driver = findEngineDriver(connection, extensions) || driverBase; - const dmp = driver.createDumper(); - if (procedureInfo) dmp.put('^execute %f', procedureInfo); - setSql(dmp.s); - } - } - - React.useEffect(() => { - if (sqlTemplate) { - loadTemplate(); - } - }, []); - - return sql; -} - -export default function QueryTab({ - tabid, - conid, - database, - initialArgs, - tabVisible, - toolbarPortalRef, - initialScript, - storageKey, - ...other -}) { - const loadingText = 'Loading SQL template...'; - const localStorageKey = storageKey || `tabdata_sql_${tabid}`; +export default function QueryTab({ tabid, conid, database, initialArgs, tabVisible, toolbarPortalRef, ...other }) { const { sqlTemplate } = initialArgs || {}; - const [queryText, setQueryText] = React.useState( - () => localStorage.getItem(localStorageKey) || initialScript || (sqlTemplate ? loadingText : '') - ); - const queryTextRef = React.useRef(queryText); const [sessionId, setSessionId] = React.useState(null); const [executeNumber, setExecuteNumber] = React.useState(0); const setOpenedTabs = useSetOpenedTabs(); - const openedTabs = useOpenedTabs(); const socket = useSocket(); const [busy, setBusy] = React.useState(false); const saveFileModalState = useModalState(); + const { editorData, setEditorData, isLoading } = useEditorData(tabid); + + const [sqlFromTemplate, isLoadingTemplate] = useSqlTemplate(sqlTemplate, { conid, database, ...other }); + const editorRef = React.useRef(null); - const sqlFromTemplate = useSqlTemplate(sqlTemplate, { conid, database, ...other }); React.useEffect(() => { - if (sqlFromTemplate && queryText == loadingText) { - editorRef.current.editor.setValue(sqlFromTemplate); - editorRef.current.editor.clearSelection(); + if (sqlFromTemplate && sqlTemplate) { + setEditorData(sqlFromTemplate); + // editorRef.current.editor.setValue(sqlFromTemplate); + // editorRef.current.editor.clearSelection(); + changeTab(tabid, setOpenedTabs, (tab) => ({ + ...tab, + props: _.omit(tab.props, ['initialArgs']), + })); } }, [sqlFromTemplate]); - const saveToStorage = React.useCallback(() => { - try { - localStorage.setItem(localStorageKey, queryTextRef.current); - } catch (err) { - console.error(err); - } - }, [localStorageKey, queryTextRef]); - const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 5000), [saveToStorage]); - - React.useEffect(() => { - window.addEventListener('beforeunload', saveToStorage); - return () => { - saveToStorage(); - window.removeEventListener('beforeunload', saveToStorage); - }; - }, []); - const handleSessionDone = React.useCallback(() => { setBusy(false); }, []); @@ -124,32 +59,13 @@ export default function QueryTab({ } }, [sessionId, socket]); - React.useEffect(() => { - if (!storageKey) - changeTab(tabid, setOpenedTabs, (tab) => ({ - ...tab, - props: { - ...tab.props, - storageKey: localStorageKey, - }, - })); - }, [storageKey]); - React.useEffect(() => { changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, busy })); }, [busy]); - const editorRef = React.useRef(null); - useUpdateDatabaseForTab(tabVisible, conid, database); const connection = useConnectionInfo({ conid }); - const handleChange = (text) => { - if (text != null) queryTextRef.current = text; - setQueryText(text); - saveToStorageDebounced(); - }; - const handleExecute = async () => { if (busy) return; setExecuteNumber((num) => num + 1); @@ -167,7 +83,7 @@ export default function QueryTab({ setBusy(true); await axios.post('sessions/execute-query', { sesid, - sql: selectedText || queryText, + sql: selectedText || editorData, }); }; @@ -204,17 +120,23 @@ export default function QueryTab({ editorRef.current.editor.clearSelection(); }; + if (isLoading || isLoadingTemplate) + return ( +
+ +
+ ); + return ( <> @@ -248,14 +170,7 @@ export default function QueryTab({ />, toolbarPortalRef.current )} - localStorage.getItem(localStorageKey)} - format="text" - folder="sql" - name={openedTabs.find((x) => x.tabid == tabid).title} - onSave={(name) => changeTab(tabid, setOpenedTabs, (tab) => ({ ...tab, title: name }))} - /> + ); } diff --git a/packages/web/src/tabs/ShellTab.js b/packages/web/src/tabs/ShellTab.js index 2a6c5150a..f2e0d3206 100644 --- a/packages/web/src/tabs/ShellTab.js +++ b/packages/web/src/tabs/ShellTab.js @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import _ from 'lodash'; +import localforage from 'localforage'; import axios from '../utility/axios'; import { useSetOpenedTabs } from '../utility/globalState'; import { VerticalSplitter } from '../widgets/Splitter'; @@ -22,13 +23,13 @@ export default function ShellTab({ storageKey, ...other }) { - const localStorageKey = storageKey || `tabdata_shell_${tabid}`; - const [shellText, setShellText] = React.useState(() => localStorage.getItem(localStorageKey) || initialScript || ''); + const localStorageKey = `tabdata_shell_${tabid}`; + const [shellText, setShellText] = React.useState(initialScript || ''); const shellTextRef = React.useRef(shellText); const [busy, setBusy] = React.useState(false); const showModal = useShowModal(); - const saveToStorage = React.useCallback(() => localStorage.setItem(localStorageKey, shellTextRef.current), [ + const saveToStorage = React.useCallback(() => localforage.setItem(localStorageKey, shellTextRef.current), [ localStorageKey, shellTextRef, ]); diff --git a/packages/web/src/utility/common.js b/packages/web/src/utility/common.js index afd6699c4..e01ddabf3 100644 --- a/packages/web/src/utility/common.js +++ b/packages/web/src/utility/common.js @@ -1,4 +1,5 @@ import uuidv1 from 'uuid/v1'; +import localforage from 'localforage'; export class LoadingToken { constructor() { @@ -14,8 +15,11 @@ export function sleep(milliseconds) { return new Promise((resolve) => window.setTimeout(() => resolve(null), milliseconds)); } -export function openNewTab(setOpenedTabs, newTab) { +export async function openNewTab(setOpenedTabs, newTab, initialData = undefined) { const tabid = uuidv1(); + if (initialData) { + await localforage.setItem(`tabdata_${tabid}`, initialData); + } setOpenedTabs((files) => [ ...(files || []).map((x) => ({ ...x, selected: false })), { diff --git a/packages/web/src/utility/useEditorData.js b/packages/web/src/utility/useEditorData.js new file mode 100644 index 000000000..caeb45963 --- /dev/null +++ b/packages/web/src/utility/useEditorData.js @@ -0,0 +1,52 @@ +import React from 'react'; +import _ from 'lodash'; +import localforage from 'localforage'; + +export default function useEditorData(tabid, initialData = null) { + const localStorageKey = `tabdata_${tabid}`; + + const [value, setValue] = React.useState(initialData); + const [isLoading, setIsLoading] = React.useState(true); + + const valueRef = React.useRef(null); + + const initialLoad = async () => { + const init = await localforage.getItem(localStorageKey); + if (init) { + setValue(init); + valueRef.current = init; + } + setIsLoading(false); + }; + + React.useEffect(() => { + initialLoad(); + }, []); + + const saveToStorage = React.useCallback(async () => { + if (valueRef.current == null) return; + try { + await localforage.setItem(localStorageKey, valueRef.current); + } catch (err) { + console.error(err); + } + }, [localStorageKey, valueRef]); + + const saveToStorageDebounced = React.useMemo(() => _.debounce(saveToStorage, 500), [saveToStorage]); + + const handleChange = (newValue) => { + if (newValue != null) valueRef.current = newValue; + setValue(newValue); + saveToStorageDebounced(); + }; + + React.useEffect(() => { + window.addEventListener('beforeunload', saveToStorage); + return () => { + saveToStorage(); + window.removeEventListener('beforeunload', saveToStorage); + }; + }, []); + + return { editorData: value, setEditorData: handleChange, isLoading }; +} diff --git a/packages/web/src/utility/useSqlTemplate.js b/packages/web/src/utility/useSqlTemplate.js new file mode 100644 index 000000000..bfc769407 --- /dev/null +++ b/packages/web/src/utility/useSqlTemplate.js @@ -0,0 +1,48 @@ +import React from 'react'; + +import { getDbCore, getConnectionInfo, getSqlObjectInfo } from '../utility/metadataLoaders'; +import sqlFormatter from 'sql-formatter'; +import useExtensions from '../utility/useExtensions'; +import { driverBase, findEngineDriver } from 'dbgate-tools'; + +export default function useSqlTemplate(sqlTemplate, props) { + const [sql, setSql] = React.useState(''); + const extensions = useExtensions(); + const [isLoading, setIsLoading] = React.useState(!!sqlTemplate); + + async function loadTemplate() { + if (sqlTemplate == 'CREATE TABLE') { + const tableInfo = await getDbCore(props, props.objectTypeField || 'tables'); + const connection = await getConnectionInfo(props); + const driver = findEngineDriver(connection, extensions) || driverBase; + const dmp = driver.createDumper(); + if (tableInfo) dmp.createTable(tableInfo); + setSql(dmp.s); + } + if (sqlTemplate == 'CREATE OBJECT') { + const objectInfo = await getSqlObjectInfo(props); + if (objectInfo) { + if (objectInfo.requiresFormat && objectInfo.createSql) setSql(sqlFormatter.format(objectInfo.createSql)); + else setSql(objectInfo.createSql); + } + } + if (sqlTemplate == 'EXECUTE PROCEDURE') { + const procedureInfo = await getSqlObjectInfo(props); + const connection = await getConnectionInfo(props); + + const driver = findEngineDriver(connection, extensions) || driverBase; + const dmp = driver.createDumper(); + if (procedureInfo) dmp.put('^execute %f', procedureInfo); + setSql(dmp.s); + } + setIsLoading(false); + } + + React.useEffect(() => { + if (sqlTemplate) { + loadTemplate(); + } + }, []); + + return [sql, isLoading]; +}