mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5e4b29c1 | ||
|
|
71c70e0232 | ||
|
|
8f49c0509f | ||
|
|
5e29a051b2 | ||
|
|
8b76258260 | ||
|
|
518270e1f5 |
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
|
||||
17
src/assets/images/checkbox_checked.svg
Normal file
17
src/assets/images/checkbox_checked.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#119DFF" stroke="#0D76BF"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M15.75 5.25L6.75 14.25L2.625 10.125L3.6825 9.0675L6.75 12.1275L14.6925 4.1925L15.75 5.25Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.625" y="3.1925" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.7 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 981 B |
17
src/assets/images/checkbox_checked_light.svg
Normal file
17
src/assets/images/checkbox_checked_light.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#F3F6FA" stroke="#C8D4E3"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M15.75 5.24988L6.75 14.2499L2.625 10.1249L3.6825 9.06738L6.75 12.1274L14.6925 4.19238L15.75 5.24988Z" fill="#119DFF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.625" y="3.19238" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.45 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 996 B |
72
src/components/CheckBox.vue
Normal file
72
src/components/CheckBox.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="checkbox-container" @click.stop="onClick">
|
||||
<div v-show="!checked" class="unchecked" />
|
||||
<img
|
||||
v-show="checked && theme === 'accent'"
|
||||
:src="require('@/assets/images/checkbox_checked.svg')"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && theme === 'light'"
|
||||
:src="require('@/assets/images/checkbox_checked_light.svg')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'checkBox',
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'accent',
|
||||
validator: (value) => {
|
||||
return ['accent', 'light'].includes(value)
|
||||
}
|
||||
},
|
||||
init: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
checked: this.init
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
checked () {
|
||||
this.$emit('change', this.checked)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.checkbox-container {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.unchecked {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.unchecked:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -15,18 +15,31 @@
|
||||
Import
|
||||
</label>
|
||||
</button>
|
||||
<button class="toolbar" v-show="selectedQueries.length > 0">Export</button>
|
||||
<button class="toolbar" v-show="selectedQueries.length > 0">Delete</button>
|
||||
<button
|
||||
class="toolbar"
|
||||
v-show="selectedQueriesCount > 0"
|
||||
@click="exportQuery(selectedQueriesIds)"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
<button
|
||||
class="toolbar"
|
||||
v-show="selectedQueriesCount > 0"
|
||||
@click="showDeleteDialog(selectedQueriesIds)"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div id="toolbar-search">
|
||||
<text-field placeholder="Search query by name" width="300px"/>
|
||||
<text-field placeholder="Search query by name" width="300px" v-model="filter"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-bg">
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<div class="fixed-header" ref="name-th">
|
||||
Name
|
||||
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
|
||||
<div class="name-th">Name</div>
|
||||
</div>
|
||||
<div class="fixed-header">
|
||||
Created at
|
||||
@@ -39,18 +52,25 @@
|
||||
>
|
||||
<table ref="table">
|
||||
<tbody>
|
||||
<tr v-for="(query, index) in queries" :key="query.id" @click="openQuery(index)">
|
||||
<tr v-for="(query, index) in showedQueries" :key="query.id" @click="openQuery(index)">
|
||||
<td ref="name-td">
|
||||
{{ query.name }}
|
||||
<div class="cell-data">
|
||||
<check-box
|
||||
ref="rowCheckBox"
|
||||
:init="selectAll || selectedQueriesIds.has(query.id)"
|
||||
@change="toggleRow($event, query.id)"
|
||||
/>
|
||||
<div class="name">{{ query.name }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="second-column">
|
||||
<div class="date-container">{{ query.createdAt | date }}</div>
|
||||
<div class="icons-container">
|
||||
<rename-icon @click="showRenameDialog(index)" />
|
||||
<rename-icon @click="showRenameDialog(query.id)" />
|
||||
<copy-icon @click="duplicateQuery(index)"/>
|
||||
<export-icon @click="exportQuery(index)"/>
|
||||
<delete-icon @click="showDeleteDialog(index)"/>
|
||||
<delete-icon @click="showDeleteDialog(query.id)"/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -84,19 +104,24 @@
|
||||
<!--Delete Query dialog -->
|
||||
<modal name="delete" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Delete query
|
||||
Delete {{ deleteGroup ? 'queries' : 'query' }}
|
||||
<close-icon @click="$modal.hide('delete')"/>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
currentQueryIndex !== null
|
||||
&& currentQueryIndex >= 0
|
||||
&& currentQueryIndex < queries.length
|
||||
deleteGroup || (
|
||||
currentQueryIndex !== null
|
||||
&& currentQueryIndex >= 0
|
||||
&& currentQueryIndex < queries.length
|
||||
)
|
||||
"
|
||||
class="dialog-body"
|
||||
>
|
||||
Are you sure you want to delete
|
||||
"{{ queries[currentQueryIndex].name }}"?
|
||||
{{ deleteGroup
|
||||
? `${selectedQueriesCount} ${selectedQueriesCount > 1 ? 'queries' : 'query'}`
|
||||
: `"${queries[currentQueryIndex].name}"`
|
||||
}}?
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
|
||||
@@ -114,6 +139,7 @@ import ExportIcon from '@/components/svg/export'
|
||||
import DeleteIcon from '@/components/svg/delete'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export default {
|
||||
@@ -124,15 +150,32 @@ export default {
|
||||
ExportIcon,
|
||||
DeleteIcon,
|
||||
CloseIcon,
|
||||
TextField
|
||||
TextField,
|
||||
CheckBox
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
queries: [],
|
||||
filter: null,
|
||||
newName: null,
|
||||
currentQueryIndex: null,
|
||||
currentQueryId: null,
|
||||
errorMsg: null,
|
||||
selectedQueries: []
|
||||
selectedQueriesIds: new Set(),
|
||||
selectedQueriesCount: 0,
|
||||
selectAll: false,
|
||||
deleteGroup: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showedQueries () {
|
||||
if (!this.filter) {
|
||||
return this.queries
|
||||
} else {
|
||||
return this.queries.filter(query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0)
|
||||
}
|
||||
},
|
||||
currentQueryIndex () {
|
||||
return this.queries.findIndex(query => query.id === this.currentQueryId)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
@@ -162,15 +205,15 @@ export default {
|
||||
this.$refs['name-th'].style = `width: ${this.$refs['name-td'][0].offsetWidth}px`
|
||||
},
|
||||
openQuery (index) {
|
||||
const tab = this.queries[index]
|
||||
const tab = this.showedQueries[index]
|
||||
tab.isUnsaved = false
|
||||
this.$store.commit('addTab', tab)
|
||||
this.$store.commit('setCurrentTabId', tab.id)
|
||||
this.$router.push('/editor')
|
||||
},
|
||||
showRenameDialog (index) {
|
||||
showRenameDialog (id) {
|
||||
this.errorMsg = null
|
||||
this.currentQueryIndex = index
|
||||
this.currentQueryId = id
|
||||
this.newName = this.queries[this.currentQueryIndex].name
|
||||
this.$modal.show('rename')
|
||||
},
|
||||
@@ -190,41 +233,82 @@ export default {
|
||||
}
|
||||
},
|
||||
duplicateQuery (index) {
|
||||
const newQuery = JSON.parse(JSON.stringify(this.queries[index]))
|
||||
const newQuery = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||
newQuery.name = newQuery.name + ' Copy'
|
||||
newQuery.id = nanoid()
|
||||
newQuery.createdAt = new Date()
|
||||
this.queries.push(newQuery)
|
||||
if (this.selectAll) {
|
||||
this.selectedQueriesIds.add(newQuery.id)
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
this.saveQueriesInLocalStorage()
|
||||
},
|
||||
showDeleteDialog (index) {
|
||||
this.currentQueryIndex = index
|
||||
showDeleteDialog (id) {
|
||||
this.deleteGroup = typeof id !== 'string'
|
||||
if (!this.deleteGroup) {
|
||||
this.currentQueryId = id
|
||||
}
|
||||
this.$modal.show('delete')
|
||||
},
|
||||
deleteQuery () {
|
||||
this.$modal.hide('delete')
|
||||
const id = this.queries[this.currentQueryIndex].id
|
||||
this.queries.splice(this.currentQueryIndex, 1)
|
||||
this.saveQueriesInLocalStorage()
|
||||
const tabIndex = this.findTabIndex(id)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('deleteTab', tabIndex)
|
||||
if (!this.deleteGroup) {
|
||||
this.queries.splice(this.currentQueryIndex, 1)
|
||||
const tabIndex = this.findTabIndex(this.currentQueryId)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('deleteTab', tabIndex)
|
||||
}
|
||||
if (this.selectedQueriesIds.has(this.currentQueryId)) {
|
||||
this.selectedQueriesIds.delete(this.currentQueryId)
|
||||
}
|
||||
} else {
|
||||
this.queries = this.selectAll
|
||||
? []
|
||||
: this.queries.filter(query => !this.selectedQueriesIds.has(query.id))
|
||||
const tabs = this.$store.state.tabs
|
||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||
if (this.selectedQueriesIds.has(tabs[i].id)) {
|
||||
this.$store.commit('deleteTab', i)
|
||||
}
|
||||
}
|
||||
this.selectedQueriesIds.clear()
|
||||
}
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
this.saveQueriesInLocalStorage()
|
||||
},
|
||||
findTabIndex (id) {
|
||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
||||
},
|
||||
exportQuery (index) {
|
||||
this.currentQueryIndex = index
|
||||
let data
|
||||
let name
|
||||
|
||||
// single operation
|
||||
if (typeof index === 'number') {
|
||||
console.log('single')
|
||||
data = JSON.parse(JSON.stringify(this.showedQueries[index]))
|
||||
name = data.name
|
||||
delete data.id
|
||||
delete data.createdAt
|
||||
} else {
|
||||
// group operation
|
||||
data = this.selectAll
|
||||
? JSON.parse(JSON.stringify(this.queries))
|
||||
: this.queries.filter(query => this.selectedQueriesIds.has(query.id))
|
||||
name = 'My sqliteviz queries'
|
||||
data.forEach(query => {
|
||||
delete query.id
|
||||
delete query.createdAt
|
||||
})
|
||||
}
|
||||
// export data to file
|
||||
const downloader = this.$refs.downloader
|
||||
const currentQuery = JSON.parse(JSON.stringify(this.queries[this.currentQueryIndex]))
|
||||
delete currentQuery.id
|
||||
delete currentQuery.createdAt
|
||||
const json = JSON.stringify(currentQuery)
|
||||
const json = JSON.stringify(data, null, 4)
|
||||
const blob = new Blob([json], { type: 'octet/stream' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
downloader.href = url
|
||||
downloader.download = `${currentQuery.name}.json`
|
||||
downloader.download = `${name}.json`
|
||||
downloader.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
},
|
||||
@@ -241,6 +325,10 @@ export default {
|
||||
importedQueries.forEach(query => {
|
||||
query.id = nanoid()
|
||||
query.createdAt = new Date()
|
||||
if (this.selectAll) {
|
||||
this.selectedQueriesIds.add(query.id)
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
})
|
||||
|
||||
this.queries = this.queries.concat(importedQueries)
|
||||
@@ -251,6 +339,24 @@ export default {
|
||||
},
|
||||
saveQueriesInLocalStorage () {
|
||||
localStorage.setItem('myQueries', JSON.stringify(this.queries))
|
||||
},
|
||||
toggleSelectAll (checked) {
|
||||
this.selectAll = checked
|
||||
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
|
||||
this.selectedQueriesIds = checked ? new Set(this.queries.map(query => query.id)) : new Set()
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
},
|
||||
toggleRow (checked, id) {
|
||||
if (checked) {
|
||||
this.selectedQueriesIds.add(id)
|
||||
} else {
|
||||
if (this.selectedQueriesIds.size === this.queries.length) {
|
||||
this.$refs.mainCheckBox.checked = false
|
||||
this.selectAll = false
|
||||
}
|
||||
this.selectedQueriesIds.delete(id)
|
||||
}
|
||||
this.selectedQueriesCount = this.selectedQueriesIds.size
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,24 +381,43 @@ export default {
|
||||
max-width: 1500px;
|
||||
width: 100%;
|
||||
}
|
||||
.fixed-header:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.fixed-header:first-child .name-th {
|
||||
margin-left: 24px;
|
||||
}
|
||||
table {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 24px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
tbody tr td:first-child {
|
||||
width: 70%;
|
||||
max-width: 0;
|
||||
padding: 0 12px;
|
||||
}
|
||||
tbody tr td:last-child {
|
||||
width: 30%;
|
||||
max-width: 0;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
tbody .cell-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
tbody .cell-data div.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
tbody tr:hover td {
|
||||
|
||||
Reference in New Issue
Block a user