Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat open, edit, and save file in query tab #785

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 18 additions & 0 deletions src/common/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const shortcutEvents: Record<string, { l18n: string; l18nParam?: string |
'kill-query': { l18n: 'database.killQuery', context: 'tab' },
'query-history': { l18n: 'database.queryHistory', context: 'tab' },
'clear-query': { l18n: 'database.clearQuery', context: 'tab' },
// 'save-file': { l18n: 'application.saveFile', context: 'tab' },
'open-file': { l18n: 'application.openFile', context: 'tab' },
'save-file-as': { l18n: 'application.saveFileAs', context: 'tab' },
'next-tab': { l18n: 'application.nextTab' },
'prev-tab': { l18n: 'application.previousTab' },
'open-all-connections': { l18n: 'application.openAllConnections' },
Expand Down Expand Up @@ -119,6 +122,21 @@ const shortcuts: ShortcutRecord[] = [
event: 'toggle-console',
keys: ['CommandOrControl+`'],
os: ['darwin', 'linux', 'win32']
},
// {
// event: 'save-file',
// keys: ['CommandOrControl+S'],
// os: ['darwin', 'linux', 'win32']
// },
{
event: 'open-file',
keys: ['CommandOrControl+O'],
os: ['darwin', 'linux', 'win32']
},
{
event: 'save-file-as',
keys: ['Shift+CommandOrControl+S'],
os: ['darwin', 'linux', 'win32']
}
];

Expand Down
28 changes: 28 additions & 0 deletions src/main/ipc-handlers/application.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { app, dialog, ipcMain, safeStorage } from 'electron';
import * as Store from 'electron-store';
import * as fs from 'fs';

import { validateSender } from '../libs/misc/validateSender';
import { ShortcutRegister } from '../libs/ShortcutRegister';
Expand Down Expand Up @@ -52,6 +53,11 @@ export default () => {
return dialog.showOpenDialog(options);
});

ipcMain.handle('show-save-dialog', (event, options) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
return dialog.showSaveDialog(options);
});

ipcMain.handle('get-download-dir-path', (event) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
return app.getPath('downloads');
Expand Down Expand Up @@ -80,4 +86,26 @@ export default () => {
const shortCutRegister = ShortcutRegister.getInstance();
shortCutRegister.unregister();
});

ipcMain.handle('read-file', (event, filePath) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
const content = fs.readFileSync(filePath, 'utf-8');
return content;
}
catch (error) {
return { status: 'error', response: error.toString() };
}
});

ipcMain.handle('write-file', (event, filePath, content) => {
if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' };
try {
fs.writeFileSync(filePath, content, 'utf-8');
return { status: 'success' };
}
catch (error) {
return { status: 'error', response: error.toString() };
}
});
};
2 changes: 1 addition & 1 deletion src/renderer/components/ScratchpadNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<div class="tile-icon">
<BaseIcon
:icon-name="note.type === 'query'
? 'mdiStarOutline'
? 'mdiHeartOutline'
: note.type === 'todo'
? note.isArchived
? 'mdiCheckboxMarkedOutline'
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/Workspace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
>
<BaseIcon
class="mt-1 mr-1"
icon-name="mdiCodeTags"
:icon-name="element.filePath ? 'mdiFileCodeOutline' : 'mdiCodeTags'"
:size="18"
/>
<span>
<span>{{ cutText(element.content || 'Query', 20, true) }} #{{ element.index }}</span>
<span>{{ cutText(element.elementName || element.content || 'Query', 20, true) }} #{{ element.index }}</span>
<span
class="btn btn-clear"
:title="t('general.close')"
Expand Down
127 changes: 125 additions & 2 deletions src/renderer/components/WorkspaceTabQuery.vue
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@
<BaseIcon icon-name="mdiStarOutline" :size="24" />
</button>
</div>
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0"
:disabled="!filePath"
:title="t('application.saveFile')"
@click="saveFile()"
>
<BaseIcon icon-name="mdiContentSaveCheckOutline" :size="24" />
</button>
<button
class="btn btn-dark btn-sm mr-0"
:title="t('application.saveFileAs')"
@click="saveFileAs()"
>
<BaseIcon icon-name="mdiContentSavePlusOutline" :size="24" />
</button>
<button
class="btn btn-dark btn-sm"
:title="t('application.openFile')"
@click="openFile()"
>
<BaseIcon icon-name="mdiFolderOpenOutline" :size="24" />
</button>
</div>
<div class="dropdown table-dropdown pr-2">
<button
:disabled="!hasResults || isQuering"
Expand Down Expand Up @@ -262,6 +286,7 @@ import QueryEditor from '@/components/QueryEditor.vue';
import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyState.vue';
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue';
import { useResultTables } from '@/composables/useResultTables';
import Application from '@/ipc-api/Application';
import Schema from '@/ipc-api/Schema';
import { useApplicationStore } from '@/stores/application';
import { useConsoleStore } from '@/stores/console';
Expand Down Expand Up @@ -302,13 +327,16 @@ const {
getWorkspace,
changeBreadcrumbs,
updateTabContent,
setUnsavedChanges
setUnsavedChanges,
newTab
} = workspacesStore;

const queryEditor: Ref<Component & { editor: Ace.Editor; $el: HTMLElement }> = ref(null);
const queryAreaFooter: Ref<HTMLDivElement> = ref(null);
const resizer: Ref<HTMLDivElement> = ref(null);
const queryName = ref('');
const query = ref('');
const filePath = ref('');
const lastQuery = ref('');
const isCancelling = ref(false);
const showCancel = ref(false);
Expand Down Expand Up @@ -339,11 +367,32 @@ watch(query, (val) => {

debounceTimeout.value = setTimeout(() => {
updateTabContent({
elementName: queryName.value,
filePath: filePath.value,
uid: props.connection.uid,
tab: props.tab.uid,
type: 'query',
schema: selectedSchema.value,
content: val

});

isQuerySaved.value = false;
}, 200);
});

watch(queryName, (val) => {
clearTimeout(debounceTimeout.value);

debounceTimeout.value = setTimeout(() => {
updateTabContent({
elementName: val,
filePath: filePath.value,
uid: props.connection.uid,
tab: props.tab.uid,
type: 'query',
schema: selectedSchema.value,
content: query.value
});

isQuerySaved.value = false;
Expand Down Expand Up @@ -529,7 +578,8 @@ const saveQuery = () => {
type: 'query',
date: new Date(),
note: query.value,
isArchived: false
isArchived: false,
title: queryName.value
});
isQuerySaved.value = true;
};
Expand Down Expand Up @@ -596,6 +646,8 @@ const rollbackTab = async () => {
defineExpose({ resizeResults });

query.value = props.tab.content as string;
queryName.value = props.tab.elementName as string;
filePath.value = props.tab.filePath as string;
selectedSchema.value = props.tab.schema || breadcrumbsSchema.value;

window.addEventListener('resize', onWindowResize);
Expand Down Expand Up @@ -630,6 +682,68 @@ const historyListener = () => {
openHistoryModal();
};

const openFileListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen)
openFile();
};

const saveFileAsListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen)
saveFileAs();
};

const saveContentListener = () => {
const hasModalOpen = !!document.querySelectorAll('.modal.active').length;
if (props.isSelected && !hasModalOpen && filePath)
saveFile();
};

const openFile = async () => {
const result = await Application.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'SQL', extensions: ['sql', 'txt'] }] });
if (result && !result.canceled) {
const file = result.filePaths[0];
const content = await Application.readFile(file);
const fileName = file.split('/').pop().split('\\').pop();
if (props.tab.filePath && props.tab.filePath !== file) {
newTab({
uid: props.connection.uid,
type: 'query',
filePath: file,
content: '',
schema: selectedSchema.value,
elementName: fileName
});
}
else {
filePath.value = file;
queryName.value = fileName;
query.value = content;
}
}
};

const saveFileAs = async () => {
const result = await Application.showSaveDialog({ filters: [{ name: 'SQL', extensions: ['sql'] }], defaultPath: `${queryName.value || 'query'}.sql` });
if (result && !result.canceled) {
await Application.writeFile(result.filePath, query.value);
addNotification({ status: 'success', message: t('general.actionSuccessful', { action: 'SAVE FILE' }) });
queryName.value = result.filePath.split('/').pop().split('\\').pop();
filePath.value = result.filePath;
}
};

const saveFile = async () => {
await Application.writeFile(filePath.value, query.value);
addNotification({ status: 'success', message: t('general.actionSuccessful', { action: 'SAVE FILE' }) });
};

const loadFileContent = async (file: string) => {
const content = await Application.readFile(file);
query.value = content;
};

onMounted(() => {
const localResizer = resizer.value;

Expand All @@ -638,6 +752,9 @@ onMounted(() => {
ipcRenderer.on('kill-query', killQueryListener);
ipcRenderer.on('clear-query', clearQueryListener);
ipcRenderer.on('query-history', historyListener);
ipcRenderer.on('open-file', openFileListener);
ipcRenderer.on('save-file-as', saveFileAsListener);
ipcRenderer.on('save-content', saveContentListener);

localResizer.addEventListener('mousedown', (e: MouseEvent) => {
e.preventDefault();
Expand All @@ -648,6 +765,9 @@ onMounted(() => {

if (props.tab.autorun)
runQuery(query.value);

if (props.tab.filePath)
loadFileContent(props.tab.filePath);
});

onBeforeUnmount(() => {
Expand All @@ -663,6 +783,9 @@ onBeforeUnmount(() => {
ipcRenderer.removeListener('kill-query', killQueryListener);
ipcRenderer.removeListener('clear-query', clearQueryListener);
ipcRenderer.removeListener('query-history', historyListener);
ipcRenderer.removeListener('open-file', openFileListener);
ipcRenderer.removeListener('save-file-as', saveFileAsListener);
ipcRenderer.removeListener('save-content', saveContentListener);
});
</script>

Expand Down
6 changes: 5 additions & 1 deletion src/renderer/i18n/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,11 @@ export const enUS = {
editNote: 'Edit note',
showArchivedNotes: 'Show archived notes',
hideArchivedNotes: 'Hide archived notes',
tag: 'Tag' // Note tag
tag: 'Tag', // Note tag,
saveFile: 'Save file',
saveFileAs: 'Save file as',
openFile: 'Open file'

},
faker: { // Faker.js methods, used in random generated content
address: 'Address',
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/ipc-api/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default class {
return ipcRenderer.invoke('show-open-dialog', unproxify(options));
}

static showSaveDialog (options: OpenDialogOptions): Promise<OpenDialogReturnValue> {
return ipcRenderer.invoke('show-save-dialog', unproxify(options));
}

static getDownloadPathDirectory (): Promise<string> {
return ipcRenderer.invoke('get-download-dir-path');
}
Expand All @@ -27,4 +31,12 @@ export default class {
static unregisterShortcuts () {
return ipcRenderer.invoke('unregister-shortcuts');
}

static readFile (path: string): Promise<string> {
return ipcRenderer.invoke('read-file', path);
}

static writeFile (path: string, content:any) {
return ipcRenderer.invoke('write-file', path, content);
}
}
Loading