This commit is contained in:
Fabio Di Stasio 2024-01-13 16:30:21 +01:00
commit 4e98dc21d8
32 changed files with 1242 additions and 160 deletions

View File

@ -15,7 +15,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

View File

@ -1,9 +1,9 @@
name: Test end-to-end [WINDOWS] name: Test end-to-end
on: on:
push: push:
branches: branches:
- master - develop
jobs: jobs:
release: release:
@ -20,7 +20,7 @@ jobs:
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 20
- name: Install dependencies - name: Install dependencies
run: npm i run: npm i

9
.vscode/launch.json vendored
View File

@ -17,15 +17,6 @@
"sourceMaps": true, "sourceMaps": true,
"type": "chrome", "type": "chrome",
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}"
},
{
"name": "Electron: Worker",
"cwd": "${workspaceFolder}",
"port": 9224,
"request": "attach",
"sourceMaps": true,
"type": "node",
"timeout": 1000000
} }
], ],
"compounds": [ "compounds": [

View File

@ -2,6 +2,31 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.7.21-beta.1](https://github.com/antares-sql/antares/compare/v0.7.21-beta.0...v0.7.21-beta.1) (2024-01-06)
### Bug Fixes
* **PostgreSQL:** error adding MONEY fields to a table ([0f8d2cb](https://github.com/antares-sql/antares/commit/0f8d2cb4ef5c327f96f788179be0b309689b4ce8))
* **PostgreSQL:** exception deleting a table with one or less tabs open ([23946ff](https://github.com/antares-sql/antares/commit/23946ff2cef6d63e1529e2c8c4357d7fdedc3284))
* **PostgreSQL:** unhandled error on connection lost, fixes [#740](https://github.com/antares-sql/antares/issues/740) ([cdd2a11](https://github.com/antares-sql/antares/commit/cdd2a11f8e33d6607337989723774d60c7c1a030))
### [0.7.21-beta.0](https://github.com/antares-sql/antares/compare/v0.7.20...v0.7.21-beta.0) (2023-12-25)
### Features
* ability to edit notes ([08e5a13](https://github.com/antares-sql/antares/commit/08e5a13f723bc3ae95b0f529b79f0b558bc2a377))
* buttons to save and access to saved queryes from query tab ([a52fc3f](https://github.com/antares-sql/antares/commit/a52fc3fd923fec30cfdd3d804554e6fe4534c400))
* highlithg sql in notes, history and console ([bfa3924](https://github.com/antares-sql/antares/commit/bfa3924d57c2ea2cc2857006d6bd6279865dbc99))
* new notes system ([eaaf1b7](https://github.com/antares-sql/antares/commit/eaaf1b756a6b5ffb77f7f07f3e4c0971822d48c3))
* open saved queries in a tab ([9a732ea](https://github.com/antares-sql/antares/commit/9a732ea1971d223f3278ad02d3dd77837fecb377))
### Bug Fixes
* JavaScript error at first startup, fixes [#736](https://github.com/antares-sql/antares/issues/736) ([b734b24](https://github.com/antares-sql/antares/commit/b734b246795fb240f6728714be68c22cc221bbe9))
### [0.7.20](https://github.com/antares-sql/antares/compare/v0.7.20-beta.2...v0.7.20) (2023-12-08) ### [0.7.20](https://github.com/antares-sql/antares/compare/v0.7.20-beta.2...v0.7.20) (2023-12-08)

View File

@ -7,7 +7,7 @@
# Antares SQL Client # Antares SQL Client
![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/fabio286/antares) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ffabio286%2Fantares%2Fbadge&style=flat)](https://actions-badge.atrox.dev/fabio286/antares/goto) ![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Twitter Follow](https://img.shields.io/twitter/follow/AntaresSQL?style=social)](https://twitter.com/AntaresSQL) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet) ![GitHub package.json version](https://img.shields.io/github/package-json/v/fabio286/antares) ![GitHub](https://img.shields.io/github/license/fabio286/antares) ![Test e2e](https://github.com/antares-sql/antares/actions/workflows/test-e2e-win.yml/badge.svg?branch=develop) ![Mastodon Follow](https://img.shields.io/mastodon/follow/%20110860460902482117?domain=https%3A%2F%2Ffosstodon.org&style=social) [![Plant a Tree](https://raw.githubusercontent.com/Fabio286/treedom-badge/master/svg/plant-a-tree.svg)](https://www.treedom.net/en/user/fabio-di-stasio/event/antares-for-the-planet)
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers. Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
Our target is to support as many databases as possible, and all major operating systems, including the ARM versions. Our target is to support as many databases as possible, and all major operating systems, including the ARM versions.
@ -17,7 +17,7 @@ However, there are all the features necessary to have a pleasant database manage
We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible. We are actively working on it, hoping to provide new cool features, improvements and fixes as soon as possible.
🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest). 🔗 If you are curious to try Antares you can download and install the [latest release](https://github.com/Fabio286/antares/releases/latest).
👁 To stay tuned for new releases follow Antares SQL on [Mastodon](https://fosstodon.org/@AntaresSQL) or [Twitter](https://twitter.com/AntaresSQL). 👁 To stay tuned for new releases follow Antares SQL on [Mastodon](https://fosstodon.org/@AntaresSQL).
🌟 Don't forget to **leave a star** if you appreciate this project. 🌟 Don't forget to **leave a star** if you appreciate this project.
🗳️ Polls: 🗳️ Polls:
@ -35,6 +35,7 @@ We are actively working on it, hoping to provide new cool features, improvements
- Fake table data filler to generate tons of data for test purpose. - Fake table data filler to generate tons of data for test purpose.
- Query suggestions and auto complete. - Query suggestions and auto complete.
- Query history: search through the last 1000 queries. - Query history: search through the last 1000 queries.
- Save queries, notes or todo.
- SSH tunnel support. - SSH tunnel support.
- Manual commit mode. - Manual commit mode.
- Import and export database dumps. - Import and export database dumps.

43
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "antares", "name": "antares",
"version": "0.7.20", "version": "0.7.21-beta.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "antares", "name": "antares",
"version": "0.7.20", "version": "0.7.21-beta.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -18,7 +18,7 @@
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vueuse/core": "~10.4.1", "@vueuse/core": "~10.4.1",
"ace-builds": "~1.24.1", "ace-builds": "~1.24.1",
"better-sqlite3": "~9.1.1", "better-sqlite3": "^8.0.1",
"electron-log": "~5.0.1", "electron-log": "~5.0.1",
"electron-store": "~8.1.0", "electron-store": "~8.1.0",
"electron-updater": "~4.6.5", "electron-updater": "~4.6.5",
@ -39,6 +39,7 @@
"source-map-support": "~0.5.20", "source-map-support": "~0.5.20",
"spectre.css": "~0.5.9", "spectre.css": "~0.5.9",
"sql-formatter": "~13.0.0", "sql-formatter": "~13.0.0",
"sql-highlight": "~4.4.0",
"v-mask": "~2.3.0", "v-mask": "~2.3.0",
"vue": "~3.3.4", "vue": "~3.3.4",
"vue-i18n": "~9.2.2", "vue-i18n": "~9.2.2",
@ -4545,13 +4546,13 @@
} }
}, },
"node_modules/better-sqlite3": { "node_modules/better-sqlite3": {
"version": "9.1.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.1.1.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.0.1.tgz",
"integrity": "sha512-FhW7bS7cXwkB2SFnPJrSGPmQerVSCzwBgmQ1cIRcYKxLsyiKjljzCbyEqqhYXo5TTBqt5BISiBj2YE2Sy2ynaA==", "integrity": "sha512-JhTZjpyapA1icCEjIZB4TSSgkGdFgpWZA2Wszg7Cf4JwJwKQmbvuNnJBeR+EYG/Z29OXvR4G//Rbg31BW/Z7Yg==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.0"
} }
}, },
"node_modules/big-integer": { "node_modules/big-integer": {
@ -14252,6 +14253,21 @@
"sql-formatter": "bin/sql-formatter-cli.cjs" "sql-formatter": "bin/sql-formatter-cli.cjs"
} }
}, },
"node_modules/sql-highlight": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-4.4.0.tgz",
"integrity": "sha512-/DeHb9IkH7Le5PDOXaF3+QuclZTvzEo7H99o7qlTncPJCpCZEBBGqmreIv7tRVIofoXA+2gRl2an6bzk/n2jNA==",
"funding": [
"https://github.com/scriptcoded/sql-highlight?sponsor=1",
{
"type": "github",
"url": "https://github.com/sponsors/scriptcoded"
}
],
"engines": {
"node": ">=14"
}
},
"node_modules/sqlstring": { "node_modules/sqlstring": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
@ -20652,12 +20668,12 @@
} }
}, },
"better-sqlite3": { "better-sqlite3": {
"version": "9.1.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-9.1.1.tgz", "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-8.0.1.tgz",
"integrity": "sha512-FhW7bS7cXwkB2SFnPJrSGPmQerVSCzwBgmQ1cIRcYKxLsyiKjljzCbyEqqhYXo5TTBqt5BISiBj2YE2Sy2ynaA==", "integrity": "sha512-JhTZjpyapA1icCEjIZB4TSSgkGdFgpWZA2Wszg7Cf4JwJwKQmbvuNnJBeR+EYG/Z29OXvR4G//Rbg31BW/Z7Yg==",
"requires": { "requires": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.0"
} }
}, },
"big-integer": { "big-integer": {
@ -27952,6 +27968,11 @@
"nearley": "^2.20.1" "nearley": "^2.20.1"
} }
}, },
"sql-highlight": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-4.4.0.tgz",
"integrity": "sha512-/DeHb9IkH7Le5PDOXaF3+QuclZTvzEo7H99o7qlTncPJCpCZEBBGqmreIv7tRVIofoXA+2gRl2an6bzk/n2jNA=="
},
"sqlstring": { "sqlstring": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",

View File

@ -1,7 +1,7 @@
{ {
"name": "antares", "name": "antares",
"productName": "Antares", "productName": "Antares",
"version": "0.7.20", "version": "0.7.21-beta.1",
"description": "A modern, fast and productivity driven SQL client with a focus in UX.", "description": "A modern, fast and productivity driven SQL client with a focus in UX.",
"license": "MIT", "license": "MIT",
"repository": "https://github.com/antares-sql/antares.git", "repository": "https://github.com/antares-sql/antares.git",
@ -126,7 +126,7 @@
"@turf/helpers": "~6.5.0", "@turf/helpers": "~6.5.0",
"@vueuse/core": "~10.4.1", "@vueuse/core": "~10.4.1",
"ace-builds": "~1.24.1", "ace-builds": "~1.24.1",
"better-sqlite3": "~9.1.1", "better-sqlite3": "^8.0.1",
"electron-log": "~5.0.1", "electron-log": "~5.0.1",
"electron-store": "~8.1.0", "electron-store": "~8.1.0",
"electron-updater": "~4.6.5", "electron-updater": "~4.6.5",
@ -147,6 +147,7 @@
"source-map-support": "~0.5.20", "source-map-support": "~0.5.20",
"spectre.css": "~0.5.9", "spectre.css": "~0.5.9",
"sql-formatter": "~13.0.0", "sql-formatter": "~13.0.0",
"sql-highlight": "~4.4.0",
"v-mask": "~2.3.0", "v-mask": "~2.3.0",
"vue": "~3.3.4", "vue": "~3.3.4",
"vue-i18n": "~9.2.2", "vue-i18n": "~9.2.2",

View File

@ -111,6 +111,7 @@ function startRenderer (callback) {
const server = new WebpackDevServer(compiler, { const server = new WebpackDevServer(compiler, {
port: 9080, port: 9080,
hot: true,
client: { client: {
overlay: true, overlay: true,
logging: 'warn' logging: 'warn'

View File

@ -66,7 +66,7 @@ export default [
group: 'monetary', group: 'monetary',
types: [ types: [
{ {
name: 'money', name: 'MONEY',
length: false, length: false,
unsigned: true unsigned: true
} }

View File

@ -36,9 +36,15 @@ export default () => {
name: 'session', name: 'session',
fileExtension: '' fileExtension: ''
}); });
const encrypted = sessionStore.get('key') as string;
const key = safeStorage.decryptString(Buffer.from(encrypted, 'utf-8')); try {
event.returnValue = key; const encrypted = sessionStore.get('key') as string;
const key = safeStorage.decryptString(Buffer.from(encrypted, 'utf-8'));
event.returnValue = key;
}
catch (error) {
event.returnValue = false;
}
}); });
ipcMain.handle('show-open-dialog', (event, options) => { ipcMain.handle('show-open-dialog', (event, options) => {

View File

@ -232,6 +232,10 @@ export class PostgreSQLClient extends BaseClient {
await this.keepAlive(); await this.keepAlive();
}, this._keepaliveMs); }, this._keepaliveMs);
connection.on('error', err => { // Intercepts errors and converts to rejections
Promise.reject(err);
});
return connection; return connection;
} }

View File

@ -166,7 +166,7 @@ export class SQLiteClient extends BaseClient {
type: type.trim(), type: type.trim(),
schema: schema, schema: schema,
table: table, table: table,
numPrecision: [...NUMBER, ...FLOAT].includes(type) ? length : null, numLength: [...NUMBER, ...FLOAT].includes(type) ? length : null,
datePrecision: null, datePrecision: null,
charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null, charLength: ![...NUMBER, ...FLOAT].includes(type) ? length : null,
nullable: !field.notnull, nullable: !field.notnull,

View File

@ -280,7 +280,6 @@ export default defineComponent({
if (props.searchable) if (props.searchable)
searchInput.value.focus(); searchInput.value.focus();
else else
el.value.focus(); el.value.focus();

View File

@ -54,7 +54,7 @@ const updateWindow = () => {
const totalScrollHeight = props.items.length * props.itemHeight; const totalScrollHeight = props.items.length * props.itemHeight;
const offset = 50; const offset = 50;
const scrollTop = localScrollElement.value.scrollTop; const scrollTop = localScrollElement.value?.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight); const firstVisibleIndex = Math.floor(scrollTop / props.itemHeight);
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount; const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;

View File

@ -152,6 +152,14 @@
/> />
SSH SSH
</div> </div>
<div v-if="connection.readonly" class="chip bg-success mt-2">
<BaseIcon
icon-name="mdiLock"
class="mr-1"
:size="18"
/>
Read-only
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -75,7 +75,7 @@
<code <code
class="cut-text" class="cut-text"
:title="query.sql" :title="query.sql"
v-html="highlightWord(query.sql)" v-html="highlight(highlightWord(query.sql), {html: true})"
/> />
</div> </div>
<div class="tile-bottom-content"> <div class="tile-bottom-content">
@ -126,7 +126,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { Component, computed, ComputedRef, onBeforeUnmount, onMounted, onUpdated, Prop, Ref, ref, watch } from 'vue'; import { highlight } from 'sql-highlight';
import {
Component,
computed,
ComputedRef,
onBeforeUnmount,
onMounted,
onUpdated,
Prop,
Ref,
ref,
watch
} from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
@ -163,7 +175,7 @@ const localSearchTerm = ref('');
const connectionName = computed(() => getConnectionName(props.connection.uid)); const connectionName = computed(() => getConnectionName(props.connection.uid));
const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || [])); const history: ComputedRef<HistoryRecord[]> = computed(() => (getHistoryByWorkspace(props.connection.uid) || []));
const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(searchTerm.value.toLowerCase()) >= 0)); const filteredHistory = computed(() => history.value.filter(q => q.sql.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0));
watch(searchTerm, () => { watch(searchTerm, () => {
clearTimeout(searchTermInterval.value); clearTimeout(searchTermInterval.value);

View File

@ -0,0 +1,120 @@
<template>
<ConfirmModal
size="medium"
:disable-autofocus="true"
:close-on-confirm="!!localNote.note.length"
:confirm-text="t('general.save')"
@confirm="updateNote"
@hide="$emit('hide')"
>
<template #header>
<div class="d-flex">
<BaseIcon
icon-name="mdiNoteEditOutline"
class="mr-1"
:size="24"
/> {{ t('application.editNote') }}
</div>
</template>
<template #body>
<form class="form">
<div class="form-group columns">
<div class="column col-8">
<label class="form-label">{{ t('connection.connection') }}</label>
<BaseSelect
v-model="localNote.cUid"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="column col-4">
<label class="form-label">{{ t('application.tag') }}</label>
<BaseSelect
v-model="localNote.type"
class="form-select"
:options="noteTags"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ t('general.content') }} <small
v-if="localNote.type !== 'query'"
style="line-height: 1;"
class="text-gray"
>({{ t('application.markdownSupported') }})</small></label>
<BaseTextEditor
v-model="localNote.note"
:mode="editorMode"
:show-line-numbers="false"
/>
</div>
</form>
</template>
</ConfirmModal>
</template>
<script lang="ts" setup>
import { inject, onBeforeMount, PropType, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
const { t } = useI18n();
const { editNote } = useScratchpadStore();
const emit = defineEmits(['hide']);
const props = defineProps({
note: {
type: Object as PropType<ConnectionNote>,
required: true
}
});
const noteTags = inject<{code: TagCode; name: string}[]>('noteTags');
const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions');
const editorMode = ref('markdown');
const localNote: Ref<ConnectionNote> = ref({
uid: 'dummy',
cUid: null,
title: undefined,
note: '',
date: new Date(),
type: 'note',
isArchived: false
});
const updateNote = () => {
if (localNote.value.note) {
if (!localNote.value.title)// Set a default title
localNote.value.title = `${localNote.value.type.toLocaleUpperCase()}: ${localNote.value.uid}`;
localNote.value.date = new Date();
editNote(localNote.value);
emit('hide');
}
};
watch(() => localNote.value.type, () => {
if (localNote.value.type === 'query')
editorMode.value = 'sql';
else
editorMode.value = 'markdown';
});
onBeforeMount(() => {
localNote.value = JSON.parse(JSON.stringify(props.note));
});
</script>

View File

@ -0,0 +1,118 @@
<template>
<ConfirmModal
size="medium"
:disable-autofocus="true"
:close-on-confirm="!!newNote.note.length"
:confirm-text="t('general.save')"
@confirm="createNote"
@hide="$emit('hide')"
>
<template #header>
<div class="d-flex">
<BaseIcon
icon-name="mdiNotePlusOutline"
class="mr-1"
:size="24"
/> {{ t('application.addNote') }}
</div>
</template>
<template #body>
<form class="form">
<div class="form-group columns">
<div class="column col-8">
<label class="form-label">{{ t('connection.connection') }}</label>
<BaseSelect
v-model="newNote.cUid"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="column col-4">
<label class="form-label">{{ t('application.tag') }}</label>
<BaseSelect
v-model="newNote.type"
class="form-select"
:options="noteTags"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
</div>
<div class="form-group">
<label class="form-label">{{ t('general.content') }} <small
v-if="newNote.type !== 'query'"
style="line-height: 1;"
class="text-gray"
>({{ t('application.markdownSupported') }})</small></label>
<BaseTextEditor
v-model="newNote.note"
:mode="editorMode"
:show-line-numbers="false"
/>
</div>
</form>
</template>
</ConfirmModal>
</template>
<script lang="ts" setup>
import { uidGen } from 'common/libs/uidGen';
import { inject, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue';
import BaseSelect from '@/components/BaseSelect.vue';
import BaseTextEditor from '@/components/BaseTextEditor.vue';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
const { t } = useI18n();
const { addNote } = useScratchpadStore();
const emit = defineEmits(['hide']);
const noteTags = inject<{code: TagCode; name: string}[]>('noteTags');
const selectedConnection = inject<Ref<null | string>>('selectedConnection');
const selectedTag = inject<Ref<TagCode>>('selectedTag');
const connectionOptions = inject<{code: string; name: string}[]>('connectionOptions');
const editorMode = ref('markdown');
const newNote: Ref<ConnectionNote> = ref({
uid: uidGen('N'),
cUid: null,
title: undefined,
note: '',
date: new Date(),
type: 'note',
isArchived: false
});
const createNote = () => {
if (newNote.value.note) {
if (!newNote.value.title)// Set a default title
newNote.value.title = `${newNote.value.type.toLocaleUpperCase()}: ${newNote.value.uid}`;
newNote.value.date = new Date();
addNote(newNote.value);
emit('hide');
}
};
watch(() => newNote.value.type, () => {
if (newNote.value.type === 'query')
editorMode.value = 'sql';
else
editorMode.value = 'markdown';
});
newNote.value.cUid = selectedConnection.value;
if (selectedTag.value !== 'all')
newNote.value.type = selectedTag.value;
</script>

View File

@ -166,19 +166,6 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group column col-12 mb-0">
<div class="col-5 col-sm-12">
<label class="form-label">
{{ t('application.disableScratchpad') }}
</label>
</div>
<div class="col-3 col-sm-12">
<label class="form-switch d-inline-block" @click.prevent="toggleDisableScratchpad">
<input type="checkbox" :checked="disableScratchpad">
<i class="form-icon" />
</label>
</div>
</div>
<div class="form-group column col-12"> <div class="form-group column col-12">
<div class="col-5 col-sm-12"> <div class="col-5 col-sm-12">
<label class="form-label"> <label class="form-label">
@ -422,14 +409,6 @@
class="d-inline mr-1" class="d-inline mr-1"
:size="16" :size="16"
/> Mastodon</a> <a /> Mastodon</a> <a
class="c-hand"
:style="'align-items: center; display: inline-flex;'"
@click="openOutside('https://twitter.com/AntaresSQL')"
><BaseIcon
icon-name="mdiTwitter"
class="d-inline mr-1"
:size="16"
/> Twitter</a> <a
class="c-hand" class="c-hand"
:style="'align-items: center; display: inline-flex;'" :style="'align-items: center; display: inline-flex;'"
@click="openOutside('https://antares-sql.app/')" @click="openOutside('https://antares-sql.app/')"
@ -499,7 +478,6 @@ const {
restoreTabs, restoreTabs,
showTableSize, showTableSize,
disableBlur, disableBlur,
disableScratchpad,
applicationTheme, applicationTheme,
editorTheme, editorTheme,
editorFontSize editorFontSize
@ -512,7 +490,6 @@ const {
changePageSize, changePageSize,
changeRestoreTabs, changeRestoreTabs,
changeDisableBlur, changeDisableBlur,
changeDisableScratchpad,
changeAutoComplete, changeAutoComplete,
changeLineWrap, changeLineWrap,
changeExecuteSelected, changeExecuteSelected,
@ -671,10 +648,6 @@ const toggleDisableBlur = () => {
changeDisableBlur(!disableBlur.value); changeDisableBlur(!disableBlur.value);
}; };
const toggleDisableScratchpad = () => {
changeDisableScratchpad(!disableScratchpad.value);
};
const toggleAutoComplete = () => { const toggleAutoComplete = () => {
changeAutoComplete(!selectedAutoComplete.value); changeAutoComplete(!selectedAutoComplete.value);
}; };

View File

@ -0,0 +1,350 @@
<template>
<div
class="tile my-2"
tabindex="0"
@click="$emit('select-note', note.uid)"
>
<BaseIcon
v-if="isBig"
class="tile-compress c-hand"
:icon-name="isSelected ? 'mdiChevronUp' : 'mdiChevronDown'"
:size="36"
@click.stop="$emit('toggle-note', note.uid)"
/>
<div class="tile-icon">
<BaseIcon
:icon-name="note.type === 'query'
? 'mdiStarOutline'
: note.type === 'todo'
? note.isArchived
? 'mdiCheckboxMarkedOutline'
: 'mdiCheckboxBlankOutline'
: 'mdiNoteEditOutline'"
:size="36"
/>
<div class="tile-icon-type">
{{ note.type }}
</div>
</div>
<div class="tile-content">
<div class="tile-content-message" :class="[{'opened': isSelected}]">
<code
v-if="note.type === 'query'"
ref="noteParagraph"
class="tile-paragraph sql"
v-html="highlight(highlightWord(note.note), {html: true})"
/>
<div
v-else
ref="noteParagraph"
class="tile-paragraph"
v-html="parseMarkdown(highlightWord(note.note))"
/>
<div v-if="isBig && !isSelected" class="tile-paragraph-overlay" />
</div>
<div class="tile-bottom-content">
<small class="tile-subtitle">{{ getConnectionName(note.cUid) || t('general.all') }} · {{ formatDate(note.date) }}</small>
<div class="tile-history-buttons">
<button
v-if="note.type === 'todo' && !note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('archive-note', note.uid)"
>
<BaseIcon
icon-name="mdiCheck"
class="pr-1"
:size="22"
/> {{ t('general.archive') }}
</button>
<button
v-if="note.type === 'todo' && note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('restore-note', note.uid)"
>
<BaseIcon
icon-name="mdiRestore"
class="pr-1"
:size="22"
/> {{ t('general.undo') }}
</button>
<button
v-if="note.type === 'query'"
class="btn btn-link pl-1"
@click.stop="$emit('select-query', note.note)"
>
<BaseIcon
icon-name="mdiOpenInApp"
class="pr-1"
:size="22"
/> {{ t('general.select') }}
</button>
<button
v-if="note.type === 'query'"
class="btn btn-link pl-1"
@click.stop="copyText(note.note)"
>
<BaseIcon
icon-name="mdiContentCopy"
class="pr-1"
:size="22"
/> {{ t('general.copy') }}
</button>
<button
v-if=" !note.isArchived"
class="btn btn-link pl-1"
@click.stop="$emit('edit-note')"
>
<BaseIcon
icon-name="mdiPencil"
class="pr-1"
:size="22"
/> {{ t('general.edit') }}
</button>
<button class="btn btn-link pl-1" @click.stop="$emit('delete-note', note.uid)">
<BaseIcon
icon-name="mdiDeleteForever"
class="pr-1"
:size="22"
/> {{ t('general.delete') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useElementBounding } from '@vueuse/core';
import { marked } from 'marked';
import { highlight } from 'sql-highlight';
import { computed, PropType, Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import BaseIcon from '@/components/BaseIcon.vue';
import { useFilters } from '@/composables/useFilters';
import { copyText } from '@/libs/copyText';
import { useConnectionsStore } from '@/stores/connections';
import { ConnectionNote } from '@/stores/scratchpad';
const props = defineProps({
note: {
type: Object as PropType<ConnectionNote>,
required: true
},
searchTerm: {
type: String,
default: () => ''
},
selectedNote: {
type: String,
default: () => ''
}
});
const { t } = useI18n();
const { formatDate } = useFilters();
const { getConnectionName } = useConnectionsStore();
defineEmits([
'edit-note',
'delete-note',
'select-note',
'toggle-note',
'archive-note',
'restore-note',
'select-query'
]);
const noteParagraph: Ref<HTMLDivElement> = ref(null);
const noteHeight = ref(useElementBounding(noteParagraph)?.height);
const isSelected = computed(() => props.selectedNote === props.note.uid);
const isBig = computed(() => noteHeight.value > 75);
const parseMarkdown = (text: string) => {
const renderer = {
listitem (text: string) {
return `<li>${text.replace(/ *\([^)]*\) */g, '')}</li>`;
},
link (href: string, title: string, text: string) {
return `<a>${text}</a>`;
}
};
marked.use({ renderer });
return marked(text);
};
const highlightWord = (string: string) => {
string = string.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
if (props.searchTerm) {
const regexp = new RegExp(`(${props.searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
return string.replace(regexp, '<span class="text-primary text-bold">$1</span>');
}
else
return string;
};
</script>
<style scoped lang="scss">
.tile {
border-radius: $border-radius;
display: flex;
position: relative;
transition: none;
&:hover,
&:focus {
.tile-content {
.tile-bottom-content {
.tile-history-buttons {
opacity: 1;
}
}
}
}
.tile-compress {
position: absolute;
right: 2px;
top: 0px;
opacity: .7;
z-index: 2;
}
.tile-icon {
font-size: 1.2rem;
margin-left: 0.3rem;
margin-right: 0.3rem;
margin-top: 0.6rem;
width: 40px;
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
opacity: .8;
.tile-icon-type {
text-transform: uppercase;
font-size: .5rem;
}
}
.tile-content {
padding: 0.3rem;
padding-left: 0.1rem;
min-height: 75px;
display: flex;
flex-direction: column;
justify-content: space-between;
.tile-content-message{
position: relative;
&:not(.opened) {
max-height: 36px;
overflow: hidden;
}
.tile-paragraph-overlay {
height: 36px;
width: 100%;
position: absolute;
top: 0;
}
}
code, pre {
max-width: 100%;
display: inline-block;
font-size: 100%;
// color: $primary-color;
opacity: 0.8;
font-weight: 600;
white-space: break-spaces;
}
.tile-subtitle {
opacity: 0.8;
}
.tile-bottom-content {
display: flex;
justify-content: space-between;
.tile-history-buttons {
opacity: 0;
transition: opacity 0.2s;
button {
font-size: 0.7rem;
height: 1rem;
line-height: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
}
}
}
}
.theme-dark {
.tile {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$body-bg-dark);
}
&:focus {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0)70%,
#323232);
}
}
&:hover{
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$bg-color-light-dark);
}
}
}
}
.theme-light {
.tile {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
#FFFF);
}
&:hover,
&:focus {
.tile-paragraph-overlay {
background-image: linear-gradient(
to bottom,
rgba(255,0,0,0) 70%,
$bg-color-light-gray);
}
}
}
}
</style>
<style lang="scss">
.tile-paragraph {
white-space: initial;
h1, h2, h3, h4, h5, h6, p, li {
margin: 0;
}
}
</style>

View File

@ -1,66 +1,368 @@
<template> <template>
<ConfirmModal <Teleport to="#window-content">
:confirm-text="t('application.update')" <div class="modal active">
:cancel-text="t('general.close')" <a class="modal-overlay" @click.stop="hideScratchpad" />
size="large" <div ref="trapRef" class="modal-container p-0 pb-4">
:hide-footer="true" <div class="modal-header pl-2">
@hide="hideScratchpad" <div class="modal-title h6">
> <div class="d-flex">
<template #header> <BaseIcon
<div class="d-flex"> icon-name="mdiNotebookOutline"
<BaseIcon class="mr-1"
icon-name="mdiNotebookEditOutline" :size="24"
class="mr-1" />
:size="24" <span>{{ t('application.note', 2) }}</span>
/> {{ t('application.scratchpad') }} </div>
</div> </div>
</template> <a class="btn btn-clear c-hand" @click.stop="hideScratchpad" />
<template #body> </div>
<div> <div class="modal-body p-0 workspace-query-results">
<div> <div
<TextEditor ref="noteFilters"
v-model="localNotes" class="d-flex p-vcentered p-2"
editor-class="textarea-editor" style="gap: 0 10px"
mode="markdown" >
:auto-focus="true" <div style="flex: 1;">
:show-line-numbers="false" <BaseSelect
/> v-model="localConnection"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/>
</div>
<div class="btn-group btn-group-block text-uppercase">
<div
v-for="tag in [{ code: 'all', name: t('general.all') }, ...noteTags]"
:key="tag.code"
class="btn"
:class="[selectedTag === tag.code ? 'btn-primary': 'btn-dark']"
@click="setTag(tag.code)"
>
{{ tag.name }}
</div>
</div>
<div class="">
<div
class="btn px-1 tooltip tooltip-left s-rounded archived-button"
:class="[showArchived ? 'btn-primary' : 'btn-link']"
:data-tooltip="showArchived ? t('application.hideArchivedNotes') : t('application.showArchivedNotes')"
@click="showArchived = !showArchived"
>
<BaseIcon
:icon-name="!showArchived ? 'mdiArchiveEyeOutline' : 'mdiArchiveCancelOutline'"
class=""
:size="24"
/>
</div>
</div>
</div>
<div>
<div
v-show="filteredNotes.length || searchTerm.length"
ref="searchForm"
class="form-group has-icon-right m-0 p-2"
>
<input
v-model="searchTerm"
class="form-input"
type="text"
:placeholder="t('general.search')"
>
<BaseIcon
v-if="!searchTerm"
icon-name="mdiMagnify"
class="form-icon pr-2"
:size="18"
/>
<BaseIcon
v-else
icon-name="mdiBackspace"
class="form-icon c-hand pr-2"
:size="18"
@click="searchTerm = ''"
/>
</div>
</div>
<div
v-if="filteredNotes.length"
ref="tableWrapper"
class="vscroll px-2"
:style="{'height': resultsSize+'px'}"
>
<div ref="table">
<BaseVirtualScroll
ref="resultTable"
:items="filteredNotes"
:item-height="83"
:visible-height="resultsSize"
:scroll-element="scrollElement"
>
<template #default="{ items }">
<ScratchpadNote
v-for="note in items"
:key="note.uid"
:search-term="searchTerm"
:note="note"
:selected-note="selectedNote"
@select-note="selectedNote = note.uid"
@toggle-note="toggleNote"
@edit-note="startEditNote(note)"
@delete-note="deleteNote"
@archive-note="archiveNote"
@restore-note="restoreNote"
@select-query="selectQuery"
/>
</template>
</BaseVirtualScroll>
</div>
</div>
<div v-else class="empty">
<div class="empty-icon">
<BaseIcon icon-name="mdiNoteSearch" :size="48" />
</div>
<p class="empty-title h5">
{{ t('application.thereAreNoNotesYet') }}
</p>
</div>
<div
class="btn btn-primary p-0 add-button tooltip tooltip-left"
:data-tooltip="t('application.addNote')"
@click="isAddModal = true"
>
<BaseIcon
icon-name="mdiPlus"
:size="48"
/>
</div>
</div> </div>
<small class="text-gray">{{ t('application.markdownSupported') }}</small>
</div> </div>
</template> </div>
</ConfirmModal> </Teleport>
<ModalNoteNew v-if="isAddModal" @hide="isAddModal = false" />
<ModalNoteEdit
v-if="isEditModal"
:note="noteToEdit"
@hide="closeEditModal"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { Ref, ref, watch } from 'vue'; import {
Component,
computed,
ComputedRef,
onBeforeUnmount,
onMounted,
onUpdated,
provide,
Ref,
ref,
watch
} from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ConfirmModal from '@/components/BaseConfirmModal.vue';
import BaseIcon from '@/components/BaseIcon.vue'; import BaseIcon from '@/components/BaseIcon.vue';
import TextEditor from '@/components/BaseTextEditor.vue'; import BaseSelect from '@/components/BaseSelect.vue';
import BaseVirtualScroll from '@/components/BaseVirtualScroll.vue';
import ModalNoteEdit from '@/components/ModalNoteEdit.vue';
import ModalNoteNew from '@/components/ModalNoteNew.vue';
import ScratchpadNote from '@/components/ScratchpadNote.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { useScratchpadStore } from '@/stores/scratchpad'; import { useConnectionsStore } from '@/stores/connections';
import { ConnectionNote, TagCode, useScratchpadStore } from '@/stores/scratchpad';
import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n(); const { t } = useI18n();
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
const scratchpadStore = useScratchpadStore(); const scratchpadStore = useScratchpadStore();
const workspacesStore = useWorkspacesStore();
const { notes } = storeToRefs(scratchpadStore); const { connectionNotes, selectedTag } = storeToRefs(scratchpadStore);
const { changeNotes } = scratchpadStore; const { changeNotes } = scratchpadStore;
const { hideScratchpad } = applicationStore; const { hideScratchpad } = applicationStore;
const { getConnectionName } = useConnectionsStore();
const { connections } = storeToRefs(useConnectionsStore());
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { getWorkspaceTab, getWorkspace, newTab, updateTabContent } = workspacesStore;
const localNotes = ref(notes.value); const localConnection = ref(null);
const debounceTimeout: Ref<NodeJS.Timeout> = ref(null); const table: Ref<HTMLDivElement> = ref(null);
const resultTable: Ref<Component & { updateWindow: () => void }> = ref(null);
const tableWrapper: Ref<HTMLDivElement> = ref(null);
const noteFilters: Ref<HTMLInputElement> = ref(null);
const searchForm: Ref<HTMLInputElement> = ref(null);
const resultsSize = ref(1000);
const searchTermInterval: Ref<NodeJS.Timeout> = ref(null);
const scrollElement: Ref<HTMLDivElement> = ref(null);
const searchTerm = ref('');
const localSearchTerm = ref('');
const showArchived = ref(false);
const isAddModal = ref(false);
const isEditModal = ref(false);
const noteToEdit: Ref<ConnectionNote> = ref(null);
const selectedNote = ref(null);
watch(localNotes, () => { const noteTags: ComputedRef<{code: TagCode; name: string}[]> = computed(() => [
clearTimeout(debounceTimeout.value); { code: 'note', name: t('application.note') },
{ code: 'todo', name: 'TODO' },
{ code: 'query', name: 'Query' }
]);
const filteredNotes = computed(() => connectionNotes.value.filter(n => (
(n.type === selectedTag.value || selectedTag.value === 'all') &&
(n.cUid === localConnection.value || localConnection.value === null) &&
(!n.isArchived || showArchived.value) &&
(n.note.toLowerCase().search(localSearchTerm.value.toLowerCase()) >= 0)
)));
const connectionOptions = computed(() => {
return [
{ code: null, name: t('general.all') },
...connections.value.map(c => ({ code: c.uid, name: getConnectionName(c.uid) }))
];
});
debounceTimeout.value = setTimeout(() => { provide('noteTags', noteTags);
changeNotes(localNotes.value); provide('connectionOptions', connectionOptions);
provide('selectedConnection', localConnection);
provide('selectedTag', selectedTag);
const resizeResults = () => {
if (resultTable.value) {
const el = tableWrapper.value.parentElement;
if (el)
resultsSize.value = el.offsetHeight - searchForm.value.offsetHeight - noteFilters.value.offsetHeight;
resultTable.value.updateWindow();
}
};
const refreshScroller = () => resizeResults();
const setTag = (tag: string) => {
selectedTag.value = tag;
};
const toggleNote = (uid: string) => {
selectedNote.value = selectedNote.value !== uid ? uid : null;
};
const startEditNote = (note: ConnectionNote) => {
isEditModal.value = true;
noteToEdit.value = note;
};
const archiveNote = (uid: string) => {
const remappedNotes = connectionNotes.value.map(n => {
if (n.uid === uid)
n.isArchived = true;
return n;
});
changeNotes(remappedNotes);
};
const restoreNote = (uid: string) => {
const remappedNotes = connectionNotes.value.map(n => {
if (n.uid === uid)
n.isArchived = false;
return n;
});
changeNotes(remappedNotes);
};
const deleteNote = (uid: string) => {
const otherNotes = connectionNotes.value.filter(n => n.uid !== uid);
changeNotes(otherNotes);
};
const selectQuery = (query: string) => {
const workspace = getWorkspace(selectedWorkspace.value);
const selectedTab = getWorkspaceTab(workspace.selectedTab);
if (workspace.connectionStatus !== 'connected') return;
if (selectedTab.type === 'query') {
updateTabContent({
tab: selectedTab.uid,
uid: selectedWorkspace.value,
type: 'query',
content: query,
schema: workspace.breadcrumbs.schema
});
}
else {
newTab({
uid: selectedWorkspace.value,
type: 'query',
content: query,
autorun: false,
schema: workspace.breadcrumbs.schema
});
}
hideScratchpad();
};
const closeEditModal = () => {
isEditModal.value = false;
noteToEdit.value = null;
};
watch(searchTerm, () => {
clearTimeout(searchTermInterval.value);
searchTermInterval.value = setTimeout(() => {
localSearchTerm.value = searchTerm.value;
}, 200); }, 200);
}); });
onUpdated(() => {
if (table.value)
refreshScroller();
if (tableWrapper.value)
scrollElement.value = tableWrapper.value;
});
onMounted(() => {
resizeResults();
window.addEventListener('resize', resizeResults);
if (selectedWorkspace.value && selectedWorkspace.value !== 'NEW')
localConnection.value = selectedWorkspace.value;
});
onBeforeUnmount(() => {
window.removeEventListener('resize', resizeResults);
clearInterval(searchTermInterval.value);
});
</script> </script>
<style lang="scss" scoped>
.vscroll {
height: 1000px;
overflow: auto;
overflow-anchor: none;
}
.add-button{
border: none;
height: 48px;
width: 48px;
border-radius: 50%;
position: fixed;
margin-top: -40px;
margin-left: 580px;
z-index: 9;
}
.archived-button {
border-radius: 50%;
width: 36px;
height: 36px;
}
</style>

View File

@ -59,17 +59,16 @@
<div class="settingbar-bottom-elements"> <div class="settingbar-bottom-elements">
<ul class="settingbar-elements"> <ul class="settingbar-elements">
<li <li
v-if="!disableScratchpad"
v-tooltip="{ v-tooltip="{
strategy: 'fixed', strategy: 'fixed',
placement: 'right', placement: 'right',
content: t('application.scratchpad') content: t('application.note', 2)
}" }"
class="settingbar-element btn btn-link" class="settingbar-element btn btn-link"
@click="showScratchpad" @click="showScratchpad()"
> >
<BaseIcon <BaseIcon
icon-name="mdiNotebookEditOutline" icon-name="mdiNotebookOutline"
class="settingbar-element-icon text-light" class="settingbar-element-icon text-light"
:size="24" :size="24"
/> />
@ -111,7 +110,6 @@ import SettingBarConnections from '@/components/SettingBarConnections.vue';
import SettingBarContext from '@/components/SettingBarContext.vue'; import SettingBarContext from '@/components/SettingBarContext.vue';
import { useApplicationStore } from '@/stores/application'; import { useApplicationStore } from '@/stores/application';
import { SidebarElement, useConnectionsStore } from '@/stores/connections'; import { SidebarElement, useConnectionsStore } from '@/stores/connections';
import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
const { t } = useI18n(); const { t } = useI18n();
@ -120,12 +118,10 @@ localStorage.setItem('opened-folders', '[]');
const applicationStore = useApplicationStore(); const applicationStore = useApplicationStore();
const connectionsStore = useConnectionsStore(); const connectionsStore = useConnectionsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const settingsStore = useSettingsStore();
const { updateStatus } = storeToRefs(applicationStore); const { updateStatus } = storeToRefs(applicationStore);
const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore); const { getSelected: selectedWorkspace } = storeToRefs(workspacesStore);
const { connectionsOrder } = storeToRefs(connectionsStore); const { connectionsOrder } = storeToRefs(connectionsStore);
const { disableScratchpad } = storeToRefs(settingsStore);
const { showSettingModal, showScratchpad } = applicationStore; const { showSettingModal, showScratchpad } = applicationStore;
const { updateConnectionsOrder, initConnectionsOrder } = connectionsStore; const { updateConnectionsOrder, initConnectionsOrder } = connectionsStore;

View File

@ -21,7 +21,7 @@
class="titlebar-element" class="titlebar-element"
@click="openDevTools" @click="openDevTools"
> >
<BaseIcon icon-name="mdiCodeTags" :size="24" /> <BaseIcon icon-name="mdiBugPlayOutline" :size="24" />
</div> </div>
<div <div
v-if="isDevelopment" v-if="isDevelopment"

View File

@ -34,7 +34,6 @@
</div> </div>
<div :title="t('general.refresh')"> <div :title="t('general.refresh')">
<BaseIcon <BaseIcon
v-if="customizations.schemas"
icon-name="mdiRefresh" icon-name="mdiRefresh"
:size="18" :size="18"
class="c-hand mr-2" class="c-hand mr-2"

View File

@ -24,7 +24,7 @@
tabindex="0" tabindex="0"
@contextmenu.prevent="contextMenu($event, wLog)" @contextmenu.prevent="contextMenu($event, wLog)"
> >
<span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql">{{ wLog.sql }}</code> <span class="type-datetime">{{ moment(wLog.date).format('HH:mm:ss') }}</span>: <code class="query-console-log-sql" v-html="highlight(wLog.sql, {html: true})" />
</div> </div>
</div> </div>
</div> </div>
@ -47,6 +47,7 @@
<script setup lang="ts"> <script setup lang="ts">
import * as moment from 'moment'; import * as moment from 'moment';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { highlight } from 'sql-highlight';
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue'; import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -89,27 +89,37 @@
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:disabled="!query || isQuering" :disabled="!query || isQuering"
:title="t('general.format')"
@click="beautify()" @click="beautify()"
> >
<BaseIcon <BaseIcon icon-name="mdiBrush" :size="24" />
class="mr-1"
icon-name="mdiBrush"
:size="24"
/>
<span>{{ t('general.format') }}</span>
</button> </button>
<button <button
class="btn btn-dark btn-sm" class="btn btn-dark btn-sm"
:disabled="isQuering" :disabled="isQuering"
:title="t('general.history')"
@click="openHistoryModal()" @click="openHistoryModal()"
> >
<BaseIcon <BaseIcon icon-name="mdiHistory" :size="24" />
class="mr-1"
icon-name="mdiHistory"
:size="24"
/>
<span>{{ t('general.history') }}</span>
</button> </button>
<div class="btn-group">
<button
class="btn btn-dark btn-sm mr-0"
:disabled="isQuering || (isQuerySaved || query.length < 5)"
:title="t('general.save')"
@click="saveQuery()"
>
<BaseIcon icon-name="mdiContentSaveOutline" :size="24" />
</button>
<button
class="btn btn-dark btn-sm"
:disabled="isQuering"
:title="t('database.savedQueries')"
@click="openSavedModal()"
>
<BaseIcon icon-name="mdiStarOutline" :size="24" />
</button>
</div>
<div class="dropdown table-dropdown pr-2"> <div class="dropdown table-dropdown pr-2">
<button <button
:disabled="!hasResults || isQuering" :disabled="!hasResults || isQuering"
@ -237,6 +247,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import { ConnectionParams } from 'common/interfaces/antares'; import { ConnectionParams } from 'common/interfaces/antares';
import { uidGen } from 'common/libs/uidGen';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { format } from 'sql-formatter'; import { format } from 'sql-formatter';
@ -252,9 +263,11 @@ import WorkspaceTabQueryEmptyState from '@/components/WorkspaceTabQueryEmptyStat
import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue'; import WorkspaceTabQueryTable from '@/components/WorkspaceTabQueryTable.vue';
import { useResultTables } from '@/composables/useResultTables'; import { useResultTables } from '@/composables/useResultTables';
import Schema from '@/ipc-api/Schema'; import Schema from '@/ipc-api/Schema';
import { useApplicationStore } from '@/stores/application';
import { useConsoleStore } from '@/stores/console'; import { useConsoleStore } from '@/stores/console';
import { useHistoryStore } from '@/stores/history'; import { useHistoryStore } from '@/stores/history';
import { useNotificationsStore } from '@/stores/notifications'; import { useNotificationsStore } from '@/stores/notifications';
import { useScratchpadStore } from '@/stores/scratchpad';
import { useSettingsStore } from '@/stores/settings'; import { useSettingsStore } from '@/stores/settings';
import { useWorkspacesStore } from '@/stores/workspaces'; import { useWorkspacesStore } from '@/stores/workspaces';
@ -279,6 +292,8 @@ const {
const { saveHistory } = useHistoryStore(); const { saveHistory } = useHistoryStore();
const { addNotification } = useNotificationsStore(); const { addNotification } = useNotificationsStore();
const workspacesStore = useWorkspacesStore(); const workspacesStore = useWorkspacesStore();
const { showScratchpad } = useApplicationStore();
const { addNote } = useScratchpadStore();
const { consoleHeight } = storeToRefs(useConsoleStore()); const { consoleHeight } = storeToRefs(useConsoleStore());
const { executeSelected } = storeToRefs(useSettingsStore()); const { executeSelected } = storeToRefs(useSettingsStore());
@ -304,6 +319,7 @@ const resultsCount = ref(0);
const durationsCount = ref(0); const durationsCount = ref(0);
const affectedCount = ref(null); const affectedCount = ref(null);
const editorHeight = ref(200); const editorHeight = ref(200);
const isQuerySaved = ref(false);
const isHistoryOpen = ref(false); const isHistoryOpen = ref(false);
const debounceTimeout = ref(null); const debounceTimeout = ref(null);
@ -329,6 +345,8 @@ watch(query, (val) => {
schema: selectedSchema.value, schema: selectedSchema.value,
content: val content: val
}); });
isQuerySaved.value = false;
}, 200); }, 200);
}); });
@ -351,6 +369,14 @@ watch(databaseSchemas, () => {
selectedSchema.value = null; selectedSchema.value = null;
}, { deep: true }); }, { deep: true });
watch(() => props.tab.content, () => {
query.value = props.tab.content;
const editorValue = queryEditor.value.editor.session.getValue();
if (editorValue !== query.value)// If change not rendered in editor
queryEditor.value.editor.session.setValue(query.value);
});
const runQuery = async (query: string) => { const runQuery = async (query: string) => {
if (!query || isQuering.value) return; if (!query || isQuering.value) return;
isQuering.value = true; isQuering.value = true;
@ -496,6 +522,22 @@ const openHistoryModal = () => {
isHistoryOpen.value = true; isHistoryOpen.value = true;
}; };
const saveQuery = () => {
addNote({
uid: uidGen('N'),
cUid: workspace.value.uid,
type: 'query',
date: new Date(),
note: query.value,
isArchived: false
});
isQuerySaved.value = true;
};
const openSavedModal = () => {
showScratchpad('query');
};
const selectQuery = (sql: string) => { const selectQuery = (sql: string) => {
if (queryEditor.value) if (queryEditor.value)
queryEditor.value.editor.session.setValue(sql); queryEditor.value.editor.session.setValue(sql);

View File

@ -1,3 +1,12 @@
/**
* [TRANSLATION UPDATE HELPER]
* - Open a terminal in antares folder and run `npm run translation:check short-code` replacing short-code with the one you are updating.
* - The command will output which terms are missing or not translated from english.
* - Open antares folder with your editor of choice.
* - Go to antares/src/renderer/i18n/ and open the locale file you want to translate.
* - Add and translate missing terms and consider whether to translate untranslated terms.
*/
export const enUS = { export const enUS = {
general: { // General purpose terms general: { // General purpose terms
edit: 'Edit', edit: 'Edit',
@ -66,9 +75,14 @@ export const enUS = {
outputFormat: 'Output format', outputFormat: 'Output format',
singleFile: 'Single {ext} file', singleFile: 'Single {ext} file',
zipCompressedFile: 'ZIP compressed {ext} file', zipCompressedFile: 'ZIP compressed {ext} file',
copyName: 'Copy name' copyName: 'Copy name',
search: 'Search',
title: 'Title',
archive: 'Archive', // verb
undo: 'Undo'
}, },
connection: { // Database connection connection: { // Database connection
connection: 'Connection',
connectionName: 'Connection name', connectionName: 'Connection name',
hostName: 'Host name', hostName: 'Host name',
client: 'Client', client: 'Client',
@ -266,12 +280,11 @@ export const enUS = {
targetTable: 'Target table', targetTable: 'Target table',
switchDatabase: 'Switch the database', switchDatabase: 'Switch the database',
searchForElements: 'Search for elements', searchForElements: 'Search for elements',
searchForSchemas: 'Search for schemas' searchForSchemas: 'Search for schemas',
savedQueries: 'Saved queries'
}, },
application: { // Application related terms application: { // Application related terms
settings: 'Settings', settings: 'Settings',
scratchpad: 'Scratchpad',
disableScratchpad: 'Disable scratchpad',
console: 'Console', console: 'Console',
general: 'General', general: 'General',
themes: 'Themes', themes: 'Themes',
@ -342,7 +355,6 @@ export const enUS = {
saveContent: 'Save content', saveContent: 'Save content',
openAllConnections: 'Open all connections', openAllConnections: 'Open all connections',
openSettings: 'Open settings', openSettings: 'Open settings',
openScratchpad: 'Open scratchpad',
runOrReload: 'Run or reload', runOrReload: 'Run or reload',
openFilter: 'Open filter', openFilter: 'Open filter',
nextResultsPage: 'Next results page', nextResultsPage: 'Next results page',
@ -376,7 +388,14 @@ export const enUS = {
ignoreDuplicates: 'Ignore duplicates', ignoreDuplicates: 'Ignore duplicates',
wrongImportPassword: 'Wrong import password', wrongImportPassword: 'Wrong import password',
wrongFileFormat: 'Wrong file format', wrongFileFormat: 'Wrong file format',
dataImportSuccess: 'Data successfully imported' dataImportSuccess: 'Data successfully imported',
note: 'Note | Notes',
thereAreNoNotesYet: 'There are no notes yet',
addNote: 'Add note',
editNote: 'Edit note',
showArchivedNotes: 'Show archived notes',
hideArchivedNotes: 'Hide archived notes',
tag: 'Tag' // Note tag
}, },
faker: { // Faker.js methods, used in random generated content faker: { // Faker.js methods, used in random generated content
address: 'Address', address: 'Address',

View File

@ -1,4 +1,4 @@
/* stylelint-disable selector-class-pattern */ /* stylelint-disable */
@import "~spectre.css/src/variables"; @import "~spectre.css/src/variables";
@import "variables"; @import "variables";
@import "transitions"; @import "transitions";
@ -109,7 +109,6 @@ option:checked {
> div { > div {
padding: 0.1rem 0.2rem; padding: 0.1rem 0.2rem;
/* stylelint-disable-next-line value-no-vendor-prefix */
min-width: -webkit-fill-available; min-width: -webkit-fill-available;
} }
} }
@ -429,3 +428,32 @@ option:checked {
} }
} }
} }
/* sql-highlight */
code.sql {
font-family: monospace;
}
.sql-hl-keyword {
color: $primary-color;
}
.sql-hl-function {
color: darkorchid;
}
.sql-hl-number {
color: $number-color;
}
.sql-hl-string {
color: $string-color;
}
.sql-hl-special {
color: goldenrod;
}
.sql-hl-bracket {
color: darkorchid;
}

View File

@ -1,6 +1,8 @@
import { Ace } from 'ace-builds'; import { Ace } from 'ace-builds';
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import { defineStore } from 'pinia'; import { defineStore, storeToRefs } from 'pinia';
import { useScratchpadStore } from './scratchpad';
const persistentStore = new Store({ name: 'settings' }); const persistentStore = new Store({ name: 'settings' });
export type UpdateStatus = 'noupdate' | 'available' | 'checking' | 'nocheck' | 'downloading' | 'downloaded' | 'disabled' | 'link'; export type UpdateStatus = 'noupdate' | 'available' | 'checking' | 'nocheck' | 'downloading' | 'downloaded' | 'disabled' | 'link';
@ -15,14 +17,12 @@ export const useApplicationStore = defineStore('application', {
isSettingModal: false, isSettingModal: false,
isScratchpad: false, isScratchpad: false,
selectedSettingTab: 'general', selectedSettingTab: 'general',
selectedConection: {},
updateStatus: 'noupdate' as UpdateStatus, updateStatus: 'noupdate' as UpdateStatus,
downloadProgress: 0, downloadProgress: 0,
baseCompleter: [] as Ace.Completer[] // Needed to reset ace editor, due global-only ace completer baseCompleter: [] as Ace.Completer[] // Needed to reset ace editor, due global-only ace completer
}), }),
getters: { getters: {
getBaseCompleter: state => state.baseCompleter, getBaseCompleter: state => state.baseCompleter,
getSelectedConnection: state => state.selectedConection,
getDownloadProgress: state => Number(state.downloadProgress.toFixed(1)) getDownloadProgress: state => Number(state.downloadProgress.toFixed(1))
}, },
actions: { actions: {
@ -53,8 +53,12 @@ export const useApplicationStore = defineStore('application', {
hideSettingModal () { hideSettingModal () {
this.isSettingModal = false; this.isSettingModal = false;
}, },
showScratchpad () { showScratchpad (tag?: string) {
this.isScratchpad = true; this.isScratchpad = true;
if (tag) {
const { selectedTag } = storeToRefs(useScratchpadStore());
selectedTag.value = tag;
}
}, },
hideScratchpad () { hideScratchpad () {
this.isScratchpad = false; this.isScratchpad = false;

View File

@ -1,24 +1,64 @@
import * as Store from 'electron-store'; import * as Store from 'electron-store';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
const persistentStore = new Store({ name: 'notes' });
export type TagCode = 'all' | 'note' | 'todo' | 'query'
export interface ConnectionNote { export interface ConnectionNote {
uid: string; uid: string;
cUid: string | null;
title?: string;
isArchived: boolean;
type: TagCode;
note: string; note: string;
date: Date; date: Date;
} }
const persistentStore = new Store({ name: 'notes' });
// Migrate old scratchpad on new notes TODO: remove in future releases
const oldNotes = persistentStore.get('notes') as string;
if (oldNotes) {
const newNotes = persistentStore.get('connectionNotes', []) as ConnectionNote[];
newNotes.unshift({
uid: 'N:LEGACY',
cUid: null,
isArchived: false,
type: 'note',
note: oldNotes,
date: new Date()
});
persistentStore.delete('notes');
persistentStore.set('connectionNotes', newNotes);
}
export const useScratchpadStore = defineStore('scratchpad', { export const useScratchpadStore = defineStore('scratchpad', {
state: () => ({ state: () => ({
/** Global notes */ selectedTag: 'all',
notes: persistentStore.get('notes', '# HOW TO SUPPORT ANTARES\n\n- [ ] Leave a star to Antares [GitHub repo](https://github.com/antares-sql/antares)\n- [ ] Send feedbacks and advices\n- [ ] Report for bugs\n- [ ] If you enjoy, share Antares with friends\n\n# ABOUT SCRATCHPAD\n\nThis is a scratchpad where you can save your **personal notes**. It supports `markdown` format, but you are free to use plain text.\nThis content is just a placeholder, feel free to clear it to make space for your notes.\n') as string,
/** Connection specific notes */ /** Connection specific notes */
connectionNotes: persistentStore.get('connectionNotes', {}) as {[k: string]: ConnectionNote} connectionNotes: persistentStore.get('connectionNotes', []) as ConnectionNote[]
}), }),
actions: { actions: {
changeNotes (notes: string) { changeNotes (notes: ConnectionNote[]) {
this.notes = notes; this.connectionNotes = notes;
persistentStore.set('notes', this.notes); persistentStore.set('connectionNotes', this.connectionNotes);
},
addNote (note: ConnectionNote) {
this.connectionNotes = [
note,
...this.connectionNotes
];
persistentStore.set('connectionNotes', this.connectionNotes);
},
editNote (note: ConnectionNote) {
this.connectionNotes = (this.connectionNotes as ConnectionNote[]).map(n => {
if (n.uid === note.uid)
n = note;
return n;
});
persistentStore.set('connectionNotes', this.connectionNotes);
} }
} }
}); });

View File

@ -30,7 +30,6 @@ export const useSettingsStore = defineStore('settings', {
editorFontSize: settingsStore.get('editor_font_size', 'medium') as EditorFontSize, editorFontSize: settingsStore.get('editor_font_size', 'medium') as EditorFontSize,
restoreTabs: settingsStore.get('restore_tabs', true) as boolean, restoreTabs: settingsStore.get('restore_tabs', true) as boolean,
disableBlur: settingsStore.get('disable_blur', false) as boolean, disableBlur: settingsStore.get('disable_blur', false) as boolean,
disableScratchpad: settingsStore.get('disable_scratchpad', false) as boolean,
shortcuts: shortcutsStore.get('shortcuts', []) as ShortcutRecord[], shortcuts: shortcutsStore.get('shortcuts', []) as ShortcutRecord[],
defaultCopyType: settingsStore.get('default_copy_type', 'cell') as string defaultCopyType: settingsStore.get('default_copy_type', 'cell') as string
}), }),
@ -93,10 +92,6 @@ export const useSettingsStore = defineStore('settings', {
this.disableBlur = val; this.disableBlur = val;
settingsStore.set('disable_blur', this.disableBlur); settingsStore.set('disable_blur', this.disableBlur);
}, },
changeDisableScratchpad (val: boolean) {
this.disableScratchpad = val;
settingsStore.set('disable_scratchpad', this.disableScratchpad);
},
updateShortcuts (shortcuts: ShortcutRecord[]) { updateShortcuts (shortcuts: ShortcutRecord[]) {
this.shortcuts = shortcuts; this.shortcuts = shortcuts;
}, },

View File

@ -66,8 +66,8 @@ export interface Workspace {
uid: string; uid: string;
client?: ClientCode; client?: ClientCode;
database?: string; database?: string;
connectionStatus: string; connectionStatus: 'connected' | 'disconnected' | 'failed';
selectedTab: string | number; selectedTab: string;
searchTerm: string; searchTerm: string;
tabs: WorkspaceTab[]; tabs: WorkspaceTab[];
structure: WorkspaceStructure[]; structure: WorkspaceStructure[];
@ -119,12 +119,12 @@ export const useWorkspacesStore = defineStore('workspaces', {
return state.workspaces.find(workspace => workspace.uid === uid).variables.find(variable => variable.name === name); return state.workspaces.find(workspace => workspace.uid === uid).variables.find(variable => variable.name === name);
}, },
getWorkspaceTab (state) { getWorkspaceTab (state) {
return (tUid: string) => { return (tUid: string): WorkspaceTab => {
if (!this.getSelected) return; if (!this.getSelected) return;
const workspace = state.workspaces.find(workspace => workspace.uid === this.getSelected); const workspace = state.workspaces.find(workspace => workspace.uid === this.getSelected);
if ('tabs' in workspace) if ('tabs' in workspace)
return workspace.tabs.find(tab => tab.uid === tUid); return workspace.tabs.find(tab => tab.uid === tUid);
return {}; return null;
}; };
}, },
getConnected: state => { getConnected: state => {
@ -410,7 +410,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
const workspace: Workspace = { const workspace: Workspace = {
uid, uid,
connectionStatus: 'disconnected', connectionStatus: 'disconnected',
selectedTab: 0, selectedTab: '0',
searchTerm: '', searchTerm: '',
tabs: [], tabs: [],
structure: [], structure: [],
@ -629,18 +629,43 @@ export const useWorkspacesStore = defineStore('workspaces', {
: false; : false;
if (existentTab) { if (existentTab) {
this._replaceTab({ uid, tab: existentTab.uid, type, database: workspaceTabs.database, schema, elementName, elementType }); this._replaceTab({ uid,
tab: existentTab.uid,
type,
database: workspaceTabs.database,
schema,
elementName,
elementType
});
tabUid = existentTab.uid; tabUid = existentTab.uid;
} }
else { else {
tabUid = uidGen('T'); 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
});
} }
} }
break; break;
default: default:
tabUid = uidGen('T'); 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
});
break; break;
} }
@ -655,7 +680,8 @@ export const useWorkspacesStore = defineStore('workspaces', {
if (!isSelectedExistent && workspace.tabs.length) { if (!isSelectedExistent && workspace.tabs.length) {
if (workspace.customizations.database) { if (workspace.customizations.database) {
const databaseTabs = workspace.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.database); const databaseTabs = workspace.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.database);
this.selectTab({ uid, tab: databaseTabs[databaseTabs.length - 1].uid }); if (databaseTabs.length)
this.selectTab({ uid, tab: databaseTabs[databaseTabs.length - 1].uid });
} }
else else
this.selectTab({ uid, tab: workspace.tabs[workspace.tabs.length - 1].uid }); this.selectTab({ uid, tab: workspace.tabs[workspace.tabs.length - 1].uid });