improved save file experience

This commit is contained in:
Jan Prochazka 2021-01-30 18:23:05 +01:00
parent 059eabf2fa
commit 255c3e5ef4
15 changed files with 182 additions and 132 deletions

View File

@ -45,29 +45,36 @@ function buildMenu() {
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
},
},
{
label: 'New query',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
},
},
{
label: 'Open file',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_openFile()`);
},
},
{ type: 'separator' },
{ role: 'close' },
],
},
{
label: 'Window',
submenu: [
{
label: 'New query',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
},
},
{ type: 'separator' },
{
label: 'Close all tabs',
click() {
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
},
},
{ type: 'separator' },
{ role: 'minimize' },
{ role: 'close' },
],
},
// {
// label: 'Edit',
// submenu: [

View File

@ -1,17 +1,9 @@
import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function ChartToolbar({ save, modelState, dispatchModel }) {
const hasPermission = useHasPermission();
export default function ChartToolbar({ modelState, dispatchModel }) {
return (
<>
{hasPermission('files/charts/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>

View File

@ -1,18 +1,15 @@
import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function QueryDesignToolbar({
execute,
isDatabaseDefined,
busy,
save,
modelState,
dispatchModel,
isConnected,
kill,
}) {
const hasPermission = useHasPermission();
return (
<>
<ToolbarButton disabled={!isDatabaseDefined || busy} onClick={execute} icon="icon run">
@ -21,11 +18,6 @@ export default function QueryDesignToolbar({
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
Kill
</ToolbarButton>
{hasPermission('files/query/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>

View File

@ -1,17 +1,9 @@
import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function MarkdownToolbar({ save, showPreview }) {
const hasPermission = useHasPermission();
export default function MarkdownToolbar({ showPreview }) {
return (
<>
{hasPermission('files/markdown/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
<ToolbarButton onClick={showPreview} icon="icon preview">
Preview
</ToolbarButton>

View File

@ -34,11 +34,10 @@ export default function SaveFileModal({
}
};
const handleSaveAs = async filePath => {
const handleSaveToDisk = async filePath => {
const path = window.require('path');
const parsed = path.parse(filePath);
if (!parsed.ext) filePath += `.${fileExtension}`;
// if (!parsed.ext) filePath += `.${fileExtension}`;
await axios.post('files/save-as', { filePath, data, format });
modalState.close();
@ -67,11 +66,15 @@ export default function SaveFileModal({
value="Save to disk"
onClick={() => {
const file = electron.remote.dialog.showSaveDialogSync(electron.remote.getCurrentWindow(), {
filters: { name: `${fileExtension.toUpperCase()} files`, extensions: [fileExtension] },
defaultPath: filePath,
filters: [
{ name: `${fileExtension.toUpperCase()} files`, extensions: [fileExtension] },
{ name: `All files`, extensions: ['*'] },
],
defaultPath: filePath || `${name}.${fileExtension}`,
properties: ['showOverwriteConfirmation'],
});
if (file) {
handleSaveAs(file);
handleSaveToDisk(file);
}
}}
/>

View File

@ -1,12 +1,28 @@
import React from 'react';
import axios from '../utility/axios';
import { changeTab } from '../utility/common';
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
import keycodes from '../utility/keycodes';
import SaveFileToolbarButton from '../utility/SaveFileToolbarButton';
import ToolbarPortal from '../utility/ToolbarPortal';
import useHasPermission from '../utility/useHasPermission';
import SaveFileModal from './SaveFileModal';
import useModalState from './useModalState';
export default function SaveTabModal({ data, folder, format, modalState, tabid, tabVisible, fileExtension }) {
export default function SaveTabModal({
data,
folder,
format,
tabid,
tabVisible,
fileExtension,
toolbarPortalRef = undefined,
}) {
const setOpenedTabs = useSetOpenedTabs();
const openedTabs = useOpenedTabs();
const saveFileModalState = useModalState();
const hasPermission = useHasPermission();
const canSave = hasPermission(`files/${folder}/write`);
const { savedFile, savedFilePath } = openedTabs.find(x => x.tabid == tabid).props || {};
const onSave = (title, newProps) => {
@ -21,35 +37,63 @@ export default function SaveTabModal({ data, folder, format, modalState, tabid,
}));
};
const handleSave = async () => {
if (savedFile) {
await axios.post('files/save', { folder, file: savedFile, data, format });
}
if (savedFilePath) {
await axios.post('files/save-as', { filePath: savedFilePath, data, format });
}
};
const handleSaveRef = React.useRef(handleSave);
handleSaveRef.current = handleSave;
const handleKeyboard = React.useCallback(
e => {
if (e.keyCode == keycodes.s && e.ctrlKey) {
e.preventDefault();
modalState.open();
if (e.shiftKey) {
saveFileModalState.open();
} else {
if (savedFile || savedFilePath) handleSaveRef.current();
else saveFileModalState.open();
}
}
},
[modalState]
[saveFileModalState]
);
React.useEffect(() => {
if (tabVisible) {
if (tabVisible && canSave) {
document.addEventListener('keydown', handleKeyboard);
return () => {
document.removeEventListener('keydown', handleKeyboard);
};
}
}, [tabVisible, handleKeyboard]);
}, [tabVisible, handleKeyboard, canSave]);
return (
<SaveFileModal
data={data}
folder={folder}
format={format}
modalState={modalState}
name={savedFile || 'newFile'}
filePath={savedFilePath}
fileExtension={fileExtension}
onSave={onSave}
/>
<>
<SaveFileModal
data={data}
folder={folder}
format={format}
modalState={saveFileModalState}
name={savedFile || 'newFile'}
filePath={savedFilePath}
fileExtension={fileExtension}
onSave={onSave}
/>
{canSave && (
<ToolbarPortal tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef}>
<SaveFileToolbarButton
saveAs={saveFileModalState.open}
save={savedFile || savedFilePath ? handleSave : null}
tabid={tabid}
/>
</ToolbarPortal>
)}
</>
);
}

View File

@ -2,7 +2,7 @@ import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, format, isConnected, kill }) {
export default function QueryToolbar({ execute, isDatabaseDefined, busy, format, isConnected, kill }) {
const hasPermission = useHasPermission();
return (
<>
@ -15,11 +15,11 @@ export default function QueryToolbar({ execute, isDatabaseDefined, busy, save, f
<ToolbarButton disabled={!isConnected} onClick={kill} icon="icon close">
Kill
</ToolbarButton>
{hasPermission('files/sql/write') && (
{/* {hasPermission('files/sql/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
)} */}
<ToolbarButton onClick={format} icon="icon format-code">
Format
</ToolbarButton>

View File

@ -1,9 +1,7 @@
import React from 'react';
import useHasPermission from '../utility/useHasPermission';
import ToolbarButton from '../widgets/ToolbarButton';
export default function ShellToolbar({ execute, cancel, busy, edit, save, editAvailable }) {
const hasPermission = useHasPermission();
export default function ShellToolbar({ execute, cancel, busy, edit, editAvailable }) {
return (
<>
<ToolbarButton disabled={busy} onClick={execute} icon="icon run">
@ -15,11 +13,6 @@ export default function ShellToolbar({ execute, cancel, busy, edit, save, editAv
<ToolbarButton disabled={!editAvailable} onClick={edit} icon="icon show-wizard">
Show wizard
</ToolbarButton>
{hasPermission('files/shell/write') && (
<ToolbarButton onClick={save} icon="icon save">
Save
</ToolbarButton>
)}
</>
);
}

View File

@ -4,17 +4,16 @@ import { createFreeTableModel } from 'dbgate-datalib';
import useUndoReducer from '../utility/useUndoReducer';
import ReactDOM from 'react-dom';
import { useUpdateDatabaseForTab } from '../utility/globalState';
import useModalState from '../modals/useModalState';
import LoadingInfo from '../widgets/LoadingInfo';
import ErrorInfo from '../widgets/ErrorInfo';
import useEditorData from '../utility/useEditorData';
import SaveTabModal from '../modals/SaveTabModal';
import ChartEditor from '../charts/ChartEditor';
import ChartToolbar from '../charts/ChartToolbar';
import ToolbarPortal from '../utility/ToolbarPortal';
export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database, tabid }) {
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
const saveFileModalState = useModalState();
const { initialData, setEditorData, errorMessage, isLoading } = useEditorData({
tabid,
});
@ -57,21 +56,17 @@ export default function ChartTab({ tabVisible, toolbarPortalRef, conid, database
database={database}
/>
<SaveTabModal
modalState={saveFileModalState}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={modelState.value}
format="json"
folder="charts"
tabid={tabid}
fileExtension='chart'
fileExtension="chart"
/>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<ChartToolbar save={saveFileModalState.open} modelState={modelState} dispatchModel={dispatchModel} />,
toolbarPortalRef.current
)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<ChartToolbar modelState={modelState} dispatchModel={dispatchModel} />
</ToolbarPortal>
</>
);
}

View File

@ -11,10 +11,10 @@ import LoadingInfo from '../widgets/LoadingInfo';
import { useOpenedTabs, useSetOpenedTabs } from '../utility/globalState';
import useOpenNewTab from '../utility/useOpenNewTab';
import { setSelectedTabFunc } from '../utility/common';
import ToolbarPortal from '../utility/ToolbarPortal';
export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef, ...other }) {
const { editorData, setEditorData, isLoading, saveToStorage } = useEditorData({ tabid });
const saveFileModalState = useModalState();
const openedTabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs();
const openNewTab = useOpenNewTab();
@ -61,21 +61,17 @@ export default function MarkdownEditorTab({ tabid, tabVisible, toolbarPortalRef,
onKeyDown={handleKeyDown}
mode="markdown"
/>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<MarkdownToolbar save={saveFileModalState.open} showPreview={showPreview} />,
toolbarPortalRef.current
)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<MarkdownToolbar showPreview={showPreview} />
</ToolbarPortal>
<SaveTabModal
modalState={saveFileModalState}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={editorData}
format="text"
folder="markdown"
tabid={tabid}
fileExtension='md'
fileExtension="md"
/>
</>
);

View File

@ -15,7 +15,6 @@ import keycodes from '../utility/keycodes';
import { changeTab } from '../utility/common';
import useSocket from '../utility/SocketProvider';
import SaveTabModal from '../modals/SaveTabModal';
import useModalState from '../modals/useModalState';
import sqlFormatter from 'sql-formatter';
import useEditorData from '../utility/useEditorData';
import LoadingInfo from '../widgets/LoadingInfo';
@ -27,6 +26,7 @@ import { generateDesignedQuery } from '../designer/designerTools';
import useUndoReducer from '../utility/useUndoReducer';
import { StatusBarItem } from '../widgets/StatusBar';
import useTimerLabel from '../utility/useTimerLabel';
import ToolbarPortal from '../utility/ToolbarPortal';
export default function QueryDesignTab({
tabid,
@ -43,7 +43,6 @@ export default function QueryDesignTab({
const setOpenedTabs = useSetOpenedTabs();
const socket = useSocket();
const [busy, setBusy] = React.useState(false);
const saveFileModalState = useModalState();
const extensions = useExtensions();
const connection = useConnectionInfo({ conid });
const engine = findEngineDriver(connection, extensions);
@ -196,36 +195,32 @@ export default function QueryDesignTab({
)}
</ResultTabs>
</VerticalSplitter>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<QueryDesignToolbar
modelState={modelState}
dispatchModel={dispatchModel}
isDatabaseDefined={conid && database}
execute={handleExecute}
busy={busy}
// cancel={handleCancel}
// format={handleFormatCode}
save={saveFileModalState.open}
isConnected={!!sessionId}
kill={handleKill}
/>,
toolbarPortalRef.current
)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<QueryDesignToolbar
modelState={modelState}
dispatchModel={dispatchModel}
isDatabaseDefined={conid && database}
execute={handleExecute}
busy={busy}
// cancel={handleCancel}
// format={handleFormatCode}
isConnected={!!sessionId}
kill={handleKill}
/>
</ToolbarPortal>
{statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<SaveTabModal
modalState={saveFileModalState}
// modalState={saveFileModalState}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
data={modelState.value}
format="json"
folder="query"
tabid={tabid}
fileExtension='qdesign'
fileExtension="qdesign"
/>
</>
);

View File

@ -23,6 +23,7 @@ import LoadingInfo from '../widgets/LoadingInfo';
import useExtensions from '../utility/useExtensions';
import useTimerLabel from '../utility/useTimerLabel';
import { StatusBarItem } from '../widgets/StatusBar';
import ToolbarPortal from '../utility/ToolbarPortal';
function createSqlPreview(sql) {
if (!sql) return undefined;
@ -58,7 +59,6 @@ export default function QueryTab({
const setOpenedTabs = useSetOpenedTabs();
const socket = useSocket();
const [busy, setBusy] = React.useState(false);
const saveFileModalState = useModalState();
const extensions = useExtensions();
const timerLabel = useTimerLabel();
const { editorData, setEditorData, isLoading } = useEditorData({
@ -201,7 +201,7 @@ export default function QueryTab({
</ResultTabs>
)}
</VerticalSplitter>
{toolbarPortalRef &&
{/* {toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
@ -216,19 +216,31 @@ export default function QueryTab({
kill={handleKill}
/>,
toolbarPortalRef.current
)}
)} */}
{statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<QueryToolbar
isDatabaseDefined={conid && database}
execute={handleExecute}
busy={busy}
// cancel={handleCancel}
format={handleFormatCode}
// save={saveFileModalState.open}
isConnected={!!sessionId}
kill={handleKill}
/>
</ToolbarPortal>
<SaveTabModal
modalState={saveFileModalState}
toolbarPortalRef={toolbarPortalRef}
tabVisible={tabVisible}
data={editorData}
format="text"
folder="sql"
tabid={tabid}
fileExtension='sql'
fileExtension="sql"
/>
</>
);

View File

@ -14,10 +14,10 @@ import useShowModal from '../modals/showModal';
import ImportExportModal from '../modals/ImportExportModal';
import useEditorData from '../utility/useEditorData';
import SaveTabModal from '../modals/SaveTabModal';
import useModalState from '../modals/useModalState';
import LoadingInfo from '../widgets/LoadingInfo';
import useTimerLabel from '../utility/useTimerLabel';
import { StatusBarItem } from '../widgets/StatusBar';
import ToolbarPortal from '../utility/ToolbarPortal';
const configRegex = /\s*\/\/\s*@ImportExportConfigurator\s*\n\s*\/\/\s*(\{[^\n]+\})\n/;
const requireRegex = /\s*(\/\/\s*@require\s+[^\n]+)\n/g;
@ -27,7 +27,6 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, statusba
const [busy, setBusy] = React.useState(false);
const showModal = useShowModal();
const { editorData, setEditorData, isLoading } = useEditorData({ tabid });
const saveFileModalState = useModalState();
const timerLabel = useTimerLabel();
const setOpenedTabs = useSetOpenedTabs();
@ -120,32 +119,27 @@ export default function ShellTab({ tabid, tabVisible, toolbarPortalRef, statusba
/>
<RunnerOutputPane runnerId={runnerId} executeNumber={executeNumber} />
</VerticalSplitter>
{toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(
<ShellToolbar
execute={handleExecute}
busy={busy}
cancel={handleCancel}
edit={handleEdit}
editAvailable={configRegex.test(editorData || '')}
save={saveFileModalState.open}
/>,
toolbarPortalRef.current
)}
<ToolbarPortal toolbarPortalRef={toolbarPortalRef} tabVisible={tabVisible}>
<ShellToolbar
execute={handleExecute}
busy={busy}
cancel={handleCancel}
edit={handleEdit}
editAvailable={configRegex.test(editorData || '')}
/>
</ToolbarPortal>
{statusbarPortalRef &&
statusbarPortalRef.current &&
tabVisible &&
ReactDOM.createPortal(<StatusBarItem>{timerLabel.text}</StatusBarItem>, statusbarPortalRef.current)}
<SaveTabModal
modalState={saveFileModalState}
toolbarPortalRef={toolbarPortalRef}
tabVisible={tabVisible}
data={editorData}
format="text"
folder="shell"
tabid={tabid}
fileExtension='js'
fileExtension="js"
/>
</>
);

View File

@ -0,0 +1,22 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import ToolbarButton, { ToolbarDropDownButton } from '../widgets/ToolbarButton';
export default function SaveFileToolbarButton({ tabid, save, saveAs }) {
if (!saveAs) return null;
if (save) {
return (
<ToolbarDropDownButton icon="icon save" text="Save">
<DropDownMenuItem onClick={save}>Save</DropDownMenuItem>
<DropDownMenuItem onClick={saveAs}>Save As</DropDownMenuItem>
</ToolbarDropDownButton>
);
}
return (
<ToolbarButton onClick={saveAs} icon="icon save">
Save As
</ToolbarButton>
);
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
export default function ToolbarPortal({ toolbarPortalRef, tabVisible, children }) {
return (
(toolbarPortalRef &&
toolbarPortalRef.current &&
tabVisible &&
children &&
ReactDOM.createPortal(children, toolbarPortalRef.current)) ||
null
);
}