Merge pull request #785 from bagusindrayana/feat-open-edit-save-file

Feat open, edit, and save file in query tab
This commit is contained in:
Fabio Di Stasio 2024-04-08 12:49:01 +02:00 committed by GitHub
commit 099a71a189
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 213 additions and 21 deletions

View File

@ -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' },
@ -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']
}
];

View File

@ -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';
@ -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');
@ -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() };
}
});
};

View File

@ -14,7 +14,7 @@
<div class="tile-icon">
<BaseIcon
:icon-name="note.type === 'query'
? 'mdiStarOutline'
? 'mdiHeartOutline'
: note.type === 'todo'
? note.isArchived
? 'mdiCheckboxMarkedOutline'

View File

@ -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')"

View File

@ -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"
@ -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';
@ -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);
@ -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;
@ -529,7 +578,8 @@ const saveQuery = () => {
type: 'query',
date: new Date(),
note: query.value,
isArchived: false
isArchived: false,
title: queryName.value
});
isQuerySaved.value = true;
};
@ -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);
@ -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;
@ -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();
@ -648,6 +765,9 @@ onMounted(() => {
if (props.tab.autorun)
runQuery(query.value);
if (props.tab.filePath)
loadFileContent(props.tab.filePath);
});
onBeforeUnmount(() => {
@ -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>

View File

@ -399,7 +399,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',

View File

@ -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');
}
@ -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);
}
}

View File

@ -37,6 +37,7 @@ export interface WorkspaceTab {
isChanged?: boolean;
content?: string;
autorun?: boolean;
filePath?: string;
}
export interface WorkspaceStructure {
@ -492,7 +493,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
}
: workspace);
},
_addTab ({ uid, tab, content, type, autorun, schema, database, elementName, elementType }: WorkspaceTab) {
_addTab ({ uid, tab, content, type, autorun, schema, database, elementName, elementType, filePath }: WorkspaceTab) {
if (type === 'query')
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
@ -506,7 +507,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
elementName,
elementType,
content: content || '',
autorun: !!autorun
autorun: !!autorun,
filePath: filePath || ''
};
this.workspaces = (this.workspaces as Workspace[]).map(workspace => {
@ -522,14 +524,14 @@ export const useWorkspacesStore = defineStore('workspaces', {
persistentStore.set(uid, (this.workspaces as Workspace[]).find(workspace => workspace.uid === uid).tabs);
},
_replaceTab ({ uid, tab: tUid, type, schema, content, elementName, elementType }: WorkspaceTab) {
_replaceTab ({ uid, tab: tUid, type, schema, content, elementName, elementType, filePath }: WorkspaceTab) {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => {
if (workspace.uid === uid) {
return {
...workspace,
tabs: workspace.tabs.map(tab => {
if (tab.uid === tUid)
return { ...tab, type, schema, content, elementName, elementType };
return { ...tab, type, schema, content, elementName, elementType, filePath };
return tab;
})
@ -541,7 +543,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
persistentStore.set(uid, (this.workspaces as Workspace[]).find(workspace => workspace.uid === uid).tabs);
},
newTab ({ uid, content, type, autorun, schema, elementName, elementType }: WorkspaceTab) {
newTab ({ uid, content, type, autorun, schema, elementName, elementType, filePath }: WorkspaceTab) {
let tabUid;
const workspaceTabs = (this.workspaces as Workspace[]).find(workspace => workspace.uid === uid);
@ -562,7 +564,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
database: workspaceTabs.database,
schema,
elementName,
elementType
elementType,
filePath
});
break;
case 'temp-data':
@ -594,21 +597,22 @@ export const useWorkspacesStore = defineStore('workspaces', {
type: tab.type.replace('temp-', ''),
schema: tab.schema,
elementName: tab.elementName,
elementType: tab.elementType
elementType: tab.elementType,
filePath: tab.filePath
});
tabUid = uidGen('T');
this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType });
this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType, filePath });
}
else {
this._replaceTab({ uid, tab: tab.uid, type, schema, elementName, elementType });
this._replaceTab({ uid, tab: tab.uid, type, schema, elementName, elementType, filePath });
tabUid = tab.uid;
}
}
}
else {
tabUid = uidGen('T');
this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType });
this._addTab({ uid, tab: tabUid, content, type, autorun, database: workspaceTabs.database, schema, elementName, elementType, filePath });
}
}
}
@ -635,7 +639,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
database: workspaceTabs.database,
schema,
elementName,
elementType
elementType,
filePath
});
tabUid = existentTab.uid;
}
@ -649,7 +654,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
database: workspaceTabs.database,
schema,
elementName,
elementType
elementType,
filePath
});
}
}
@ -664,7 +670,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
database: workspaceTabs.database,
schema,
elementName,
elementType
elementType,
filePath
});
break;
}
@ -687,8 +694,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
this.selectTab({ uid, tab: workspace.tabs[workspace.tabs.length - 1].uid });
}
},
updateTabContent ({ uid, tab, type, schema, content }: WorkspaceTab) {
this._replaceTab({ uid, tab, type, schema, content });
updateTabContent ({ uid, tab, type, schema, content, elementName, filePath }: WorkspaceTab) {
this._replaceTab({ uid, tab, type, schema, content, elementName, filePath });
},
renameTabs ({ uid, schema, elementName, elementNewName }: WorkspaceTab) {
this.workspaces = (this.workspaces as Workspace[]).map(workspace => {