mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
adding and closing tabs; saving of queries
This commit is contained in:
3
src/assets/images/close.svg
Normal file
3
src/assets/images/close.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z" fill="#A2B1C6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
@@ -28,5 +28,9 @@ button.primary:disabled {
|
|||||||
color: var(--color-text-light-2);
|
color: var(--color-text-light-2);
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.primary:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
--color-text-light: var(--color-white);
|
--color-text-light: var(--color-white);
|
||||||
--color-text-light-2: var(--color-gray-medium);
|
--color-text-light-2: var(--color-gray-medium);
|
||||||
--color-text-base: var(--color-gray-dark);
|
--color-text-base: var(--color-gray-dark);
|
||||||
--color-text-medium: var(--color-gray-medium);
|
|
||||||
--color-text-active: var(--color-blue-dark-2);
|
--color-text-active: var(--color-blue-dark-2);
|
||||||
|
|
||||||
--shadow: 0 1px 2px rgba(42, 63, 95, 0.7);
|
--shadow: 0 1px 2px rgba(42, 63, 95, 0.7);
|
||||||
|
|||||||
@@ -5,15 +5,68 @@
|
|||||||
<router-link to="/my-queries">My queries</router-link>
|
<router-link to="/my-queries">My queries</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="primary" disabled>Save</button>
|
<button
|
||||||
<button class="primary">Create</button>
|
v-if="$store.state.tabs.length > 0"
|
||||||
|
class="primary"
|
||||||
|
:disabled="!$store.state.currentTab.isUnsaved"
|
||||||
|
@click="saveQuery"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button class="primary" @click="createNewQuery">Create</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'MainMenu'
|
name: 'MainMenu',
|
||||||
|
methods: {
|
||||||
|
createNewQuery () {
|
||||||
|
const tab = {
|
||||||
|
id: Number(new Date()),
|
||||||
|
name: this.$store.state.untitledLastIndex === 3 ? 'Very good query' : null,
|
||||||
|
tempName: this.$store.state.untitledLastIndex
|
||||||
|
? `Untitled ${this.$store.state.untitledLastIndex}`
|
||||||
|
: 'Untitled',
|
||||||
|
isUnsaved: true
|
||||||
|
}
|
||||||
|
this.$store.commit('addTab', tab)
|
||||||
|
this.$store.commit('setCurrentTabId', tab.id)
|
||||||
|
this.$store.commit('updateUntitledLastIndex')
|
||||||
|
},
|
||||||
|
saveQuery () {
|
||||||
|
const currentQuery = this.$store.state.currentTab
|
||||||
|
const isFromScratch = !this.$store.state.currentTab.initName
|
||||||
|
const value = {
|
||||||
|
id: currentQuery.id,
|
||||||
|
query: currentQuery.query
|
||||||
|
// TODO: save plotly settings
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFromScratch) {
|
||||||
|
value.name = prompt('query name')
|
||||||
|
// TODO: create dialog
|
||||||
|
this.$store.commit('updateTabName', { index: currentQuery.tabIndex, newName: value.name })
|
||||||
|
value.createdAt = new Date()
|
||||||
|
} else {
|
||||||
|
value.name = currentQuery.initName
|
||||||
|
}
|
||||||
|
|
||||||
|
let myQueries = JSON.parse(localStorage.getItem('myQueries'))
|
||||||
|
if (!myQueries) {
|
||||||
|
myQueries = [value]
|
||||||
|
} else if (isFromScratch) {
|
||||||
|
myQueries.push(value)
|
||||||
|
} else {
|
||||||
|
const queryIndex = myQueries.findIndex(query => query.id === currentQuery.id)
|
||||||
|
value.createdAt = myQueries[queryIndex].createdAt
|
||||||
|
myQueries[queryIndex] = value
|
||||||
|
}
|
||||||
|
localStorage.setItem('myQueries', JSON.stringify(myQueries))
|
||||||
|
currentQuery.isUnsaved = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<div slot="left-pane" class="query-editor">
|
<div slot="left-pane" class="query-editor">
|
||||||
<div class="codemirror-container">
|
<div class="codemirror-container">
|
||||||
<codemirror v-model="code" :options="cmOptions" @changes="onCmChange" />
|
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" ref="codemirror" />
|
||||||
</div>
|
</div>
|
||||||
<div class="run-btn-container">
|
<div class="run-btn-container">
|
||||||
<button class="primary run-btn" @click="execEditorContents">Run</button>
|
<button class="primary run-btn" @click="execEditorContents">Run</button>
|
||||||
@@ -59,7 +59,7 @@ import 'codemirror/addon/hint/sql-hint.js'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TabContent',
|
name: 'TabContent',
|
||||||
props: ['name', 'isActive'],
|
props: ['id', 'initName', 'initQuery', 'initPlotly', 'tabIndex'],
|
||||||
components: {
|
components: {
|
||||||
codemirror,
|
codemirror,
|
||||||
SqlTable,
|
SqlTable,
|
||||||
@@ -74,7 +74,7 @@ export default {
|
|||||||
layout: {},
|
layout: {},
|
||||||
frames: []
|
frames: []
|
||||||
},
|
},
|
||||||
code: 'select * from albums',
|
query: 'select * from albums',
|
||||||
cmOptions: {
|
cmOptions: {
|
||||||
// codemirror options
|
// codemirror options
|
||||||
tabSize: 4,
|
tabSize: 4,
|
||||||
@@ -86,10 +86,14 @@ export default {
|
|||||||
result: null,
|
result: null,
|
||||||
view: 'table',
|
view: 'table',
|
||||||
tableViewHeight: 0,
|
tableViewHeight: 0,
|
||||||
worker: this.$store.state.worker
|
worker: this.$store.state.worker,
|
||||||
|
isUnsaved: !this.name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isActive () {
|
||||||
|
return this.id === this.$store.state.currentTabId
|
||||||
|
},
|
||||||
dataSources () {
|
dataSources () {
|
||||||
if (!this.result) {
|
if (!this.result) {
|
||||||
return {}
|
return {}
|
||||||
@@ -110,13 +114,30 @@ export default {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
this.$store.commit('setCurrentTab', this)
|
||||||
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
new ResizeObserver(this.calculateTableHeight).observe(this.$refs.bottomPane)
|
new ResizeObserver(this.calculateTableHeight).observe(this.$refs.bottomPane)
|
||||||
this.calculateTableHeight()
|
this.calculateTableHeight()
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
isActive () {
|
||||||
|
if (this.isActive) {
|
||||||
|
this.$store.commit('setCurrentTab', this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
query () {
|
||||||
|
this.isUnsaved = true
|
||||||
|
},
|
||||||
|
isUnsaved () {
|
||||||
|
this.$store.commit('updateTabState', { index: this.tabIndex, newValue: this.isUnsaved })
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update (data, layout, frames) {
|
update (data, layout, frames) {
|
||||||
this.state = { data, layout, frames }
|
this.state = { data, layout, frames }
|
||||||
|
this.isUnsaved = true
|
||||||
console.log(this.state)
|
console.log(this.state)
|
||||||
},
|
},
|
||||||
onCmChange (editor) {
|
onCmChange (editor) {
|
||||||
@@ -154,7 +175,7 @@ export default {
|
|||||||
// this.$refs.output.textContent = 'Fetching results...'
|
// this.$refs.output.textContent = 'Fetching results...'
|
||||||
},
|
},
|
||||||
execEditorContents () {
|
execEditorContents () {
|
||||||
this.execute(this.code + ';')
|
this.execute(this.query + ';')
|
||||||
},
|
},
|
||||||
calculateTableHeight () {
|
calculateTableHeight () {
|
||||||
const bottomPane = this.$refs.bottomPane
|
const bottomPane = this.$refs.bottomPane
|
||||||
|
|||||||
@@ -2,19 +2,39 @@
|
|||||||
<div>
|
<div>
|
||||||
<div id="tabs__header">
|
<div id="tabs__header">
|
||||||
<div
|
<div
|
||||||
v-for="tab in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
@click="selectTab(tab.id)"
|
@click="selectTab(tab.id)"
|
||||||
:class='{"tab__selected": (tab.id === selectedIndex)}'
|
:class="[{'tab__selected': (tab.id === selectedIndex)}, 'tab']"
|
||||||
>
|
>
|
||||||
{{ tab.name }}
|
<div class="tab-name">
|
||||||
|
<span v-show="tab.isUnsaved">*</span>
|
||||||
|
<span v-if="tab.name">{{ tab.name }}</span>
|
||||||
|
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<svg
|
||||||
|
class="close-icon"
|
||||||
|
@click.stop="closeTab(index)"
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z"
|
||||||
|
fill="#A2B1C6"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<tab-content
|
<tab-content
|
||||||
v-for="tab in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:is-active="tab.isActive"
|
:id="tab.id"
|
||||||
:name="tab.name"
|
:init-name="tab.name"
|
||||||
|
:tab-index="index"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,19 +48,22 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
selectedIndex: 0,
|
}
|
||||||
tabs: [
|
},
|
||||||
{ id: 1, name: 'New query', isActive: true },
|
computed: {
|
||||||
{ id: 2, name: 'New query 2', isActive: false }
|
tabs () {
|
||||||
]
|
return this.$store.state.tabs
|
||||||
|
},
|
||||||
|
selectedIndex () {
|
||||||
|
return this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectTab (id) {
|
selectTab (id) {
|
||||||
this.selectedIndex = id
|
this.$store.commit('setCurrentTabId', id)
|
||||||
this.tabs.forEach(tab => {
|
},
|
||||||
tab.isActive = (tab.id === id)
|
closeTab (index) {
|
||||||
})
|
this.$store.commit('deleteTab', index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,8 +73,10 @@ export default {
|
|||||||
#tabs__header {
|
#tabs__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#tabs__header div {
|
#tabs__header .tab {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
background-color: var(--color-bg-light);
|
background-color: var(--color-bg-light);
|
||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
@@ -62,7 +87,18 @@ export default {
|
|||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
max-width: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
#tabs__header .tab-name {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#tabs__header div:hover {
|
#tabs__header div:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -84,4 +120,13 @@ export default {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon:hover path {
|
||||||
|
fill: var(--color-text-base);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ export default new Vuex.Store({
|
|||||||
schema: null,
|
schema: null,
|
||||||
dbFile: null,
|
dbFile: null,
|
||||||
dbName: null,
|
dbName: null,
|
||||||
worker: new Worker('/js/worker.sql-wasm.js')
|
worker: new Worker('/js/worker.sql-wasm.js'),
|
||||||
|
tabs: [],
|
||||||
|
currentTab: null,
|
||||||
|
currentTabId: null,
|
||||||
|
untitledLastIndex: 0
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
saveSchema (state, schema) {
|
saveSchema (state, schema) {
|
||||||
@@ -19,6 +23,41 @@ export default new Vuex.Store({
|
|||||||
},
|
},
|
||||||
saveDbName (state, name) {
|
saveDbName (state, name) {
|
||||||
state.dbName = name
|
state.dbName = name
|
||||||
|
},
|
||||||
|
addTab (state, tab) {
|
||||||
|
state.tabs.push(tab)
|
||||||
|
},
|
||||||
|
updateTabName (state, { index, newName }) {
|
||||||
|
const tab = state.tabs[index]
|
||||||
|
tab.name = newName
|
||||||
|
Vue.set(state.tabs, index, tab)
|
||||||
|
},
|
||||||
|
updateTabState (state, { index, newValue }) {
|
||||||
|
console.log(index, newValue)
|
||||||
|
const tab = state.tabs[index]
|
||||||
|
tab.isUnsaved = newValue
|
||||||
|
Vue.set(state.tabs, index, tab)
|
||||||
|
},
|
||||||
|
deleteTab (state, index) {
|
||||||
|
if (state.tabs[index].id !== state.currentTabId) {
|
||||||
|
} else if (index < state.tabs.length - 1) {
|
||||||
|
state.currentTabId = state.tabs[index + 1].id
|
||||||
|
} else if (index > 0) {
|
||||||
|
state.currentTabId = state.tabs[index - 1].id
|
||||||
|
} else {
|
||||||
|
state.currentTabId = null
|
||||||
|
state.untitledLastIndex = 0
|
||||||
|
}
|
||||||
|
state.tabs.splice(index, 1)
|
||||||
|
},
|
||||||
|
setCurrentTabId (state, id) {
|
||||||
|
state.currentTabId = id
|
||||||
|
},
|
||||||
|
setCurrentTab (state, tab) {
|
||||||
|
state.currentTab = tab
|
||||||
|
},
|
||||||
|
updateUntitledLastIndex (state) {
|
||||||
|
state.untitledLastIndex += 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user