1
1
mirror of https://github.com/Fabio286/antares.git synced 2025-06-05 21:59:22 +02:00

Merge pull request #735 from antares-sql/feat/new-scratchpad

Feat/new scratchpad
This commit is contained in:
2023-12-25 11:19:42 +01:00
committed by GitHub
23 changed files with 1178 additions and 117 deletions

View File

@@ -3,7 +3,7 @@ name: Test end-to-end [WINDOWS]
on: on:
push: push:
branches: branches:
- master - develop
jobs: jobs:
release: release:

21
package-lock.json generated
View File

@@ -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",
@@ -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",
@@ -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

@@ -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

@@ -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">
@@ -499,7 +486,6 @@ const {
restoreTabs, restoreTabs,
showTableSize, showTableSize,
disableBlur, disableBlur,
disableScratchpad,
applicationTheme, applicationTheme,
editorTheme, editorTheme,
editorFontSize editorFontSize
@@ -512,7 +498,6 @@ const {
changePageSize, changePageSize,
changeRestoreTabs, changeRestoreTabs,
changeDisableBlur, changeDisableBlur,
changeDisableScratchpad,
changeAutoComplete, changeAutoComplete,
changeLineWrap, changeLineWrap,
changeExecuteSelected, changeExecuteSelected,
@@ -671,10 +656,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,366 @@
<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">
>
<template #header>
<div class="d-flex"> <div class="d-flex">
<BaseIcon <BaseIcon
icon-name="mdiNotebookEditOutline" icon-name="mdiNotebookOutline"
class="mr-1" class="mr-1"
:size="24" :size="24"
/> {{ t('application.scratchpad') }} />
<span>{{ t('application.note', 2) }}</span>
</div> </div>
</template> </div>
<template #body> <a class="btn btn-clear c-hand" @click.stop="hideScratchpad" />
<div> </div>
<div> <div class="modal-body p-0 workspace-query-results">
<TextEditor <div
v-model="localNotes" ref="noteFilters"
editor-class="textarea-editor" class="d-flex p-vcentered p-2"
mode="markdown" style="gap: 0 10px"
:auto-focus="true" >
:show-line-numbers="false" <div style="flex: 1;">
<BaseSelect
v-model="localConnection"
class="form-select"
:options="connectionOptions"
option-track-by="code"
option-label="name"
@change="null"
/> />
</div> </div>
<small class="text-gray">{{ t('application.markdownSupported') }}</small> <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>
<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> </template>
</ConfirmModal> </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>
</div>
</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 (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

@@ -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

@@ -67,7 +67,7 @@ export interface Workspace {
client?: ClientCode; client?: ClientCode;
database?: string; database?: string;
connectionStatus: string; connectionStatus: string;
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;
} }