1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-10 03:58:54 +08:00
This commit is contained in:
lana-k
2025-03-30 15:57:47 +02:00
parent 6f7961e1b4
commit df16383d49
64 changed files with 316 additions and 279 deletions

View File

@@ -0,0 +1,649 @@
<template>
<div id="my-inquiries-container">
<div v-if="allInquiries.length === 0" id="start-guide">
You don't have saved inquiries so far.
<span class="link" @click="emitCreateTabEvent">Create</span>
the one from scratch or
<span class="link" @click="importInquiries">import</span> from a file.
</div>
<div
v-if="$store.state.loadingPredefinedInquiries"
id="loading-predefined-status"
>
<loading-indicator />
Loading predefined inquiries...
</div>
<div
v-show="allInquiries.length > 0"
id="my-inquiries-content"
ref="my-inquiries-content"
>
<div id="my-inquiries-toolbar">
<div id="toolbar-buttons">
<button
id="toolbar-btns-import"
class="toolbar"
@click="importInquiries"
>
Import
</button>
<button
v-show="selectedInquiriesCount > 0"
id="toolbar-btns-export"
class="toolbar"
@click="exportSelectedInquiries()"
>
Export
</button>
<button
v-show="selectedNotPredefinedCount > 0"
id="toolbar-btns-delete"
class="toolbar"
@click="showDeleteDialog(selectedInquiriesIds)"
>
Delete
</button>
</div>
<div id="toolbar-search">
<text-field
v-model="filter"
placeholder="Search inquiry by name"
width="300px"
/>
</div>
</div>
<div v-show="showedInquiries.length === 0" id="inquiries-not-found">
No inquiries found
</div>
<div v-show="showedInquiries.length > 0" class="rounded-bg">
<div class="header-container">
<div>
<div ref="name-th" class="fixed-header">
<check-box
ref="mainCheckBox"
theme="light"
@click="toggleSelectAll"
/>
<div class="name-th">Name</div>
</div>
<div class="fixed-header">Created at</div>
</div>
</div>
<div
class="table-container"
:style="{ 'max-height': `${maxTableHeight}px` }"
>
<table ref="table" class="sqliteviz-table">
<tbody>
<tr
v-for="(inquiry, index) in showedInquiries"
:key="inquiry.id"
@click="openInquiry(index)"
>
<td ref="name-td">
<div class="cell-data">
<check-box
ref="rowCheckBox"
:init="selectAll || selectedInquiriesIds.has(inquiry.id)"
data-test="rowCheckBox"
@click="toggleRow($event, inquiry.id)"
/>
<div class="name">{{ inquiry.name }}</div>
<div
v-if="inquiry.isPredefined"
class="badge"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
Predefined
<span
ref="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
>
Predefined inquiries come from the server. These
inquiries cant be deleted or renamed.
</span>
</div>
</div>
</td>
<td>
<div class="second-column">
<div class="date-container">
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)" />
<export-icon
tooltip="Export inquiry to file"
tooltip-position="top-left"
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog(new Set().add(inquiry.id))"
/>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!--Rename Inquiry dialog -->
<modal modal-id="rename" class="dialog" content-style="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')" />
</div>
<div class="dialog-body">
<text-field
v-model="newName"
label="New inquiry name"
:error-msg="errorMsg"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('rename')">Cancel</button>
<button class="primary" @click="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal modal-id="delete" class="dialog" content-style="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')" />
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div
v-show="selectedInquiriesCount > selectedNotPredefinedCount"
id="note"
>
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries you've selected won't be deleted
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteInquiry">Delete</button>
</div>
</modal>
</div>
</template>
<script>
import RenameIcon from './svg/rename'
import CopyIcon from './svg/copy'
import ExportIcon from '@/components/svg/export'
import DeleteIcon from './svg/delete'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries'
import eventBus from '@/lib/eventBus'
export default {
name: 'Inquiries',
components: {
RenameIcon,
CopyIcon,
ExportIcon,
DeleteIcon,
CloseIcon,
TextField,
CheckBox,
LoadingIndicator
},
mixins: [tooltipMixin],
data() {
return {
filter: null,
newName: null,
processedInquiryId: null,
errorMsg: null,
selectedInquiriesIds: new Set(),
selectedInquiriesCount: 0,
selectedNotPredefinedCount: 0,
selectAll: false,
deleteGroup: false,
resizeObserver: null,
maxTableHeight: 0
}
},
computed: {
inquiries() {
return this.$store.state.inquiries
},
predefinedInquiries() {
return this.$store.state.predefinedInquiries.map(inquiry => {
inquiry.isPredefined = true
return inquiry
})
},
predefinedInquiriesIds() {
return new Set(this.predefinedInquiries.map(inquiry => inquiry.id))
},
showedInquiries() {
let showedInquiries = this.allInquiries
if (this.filter) {
showedInquiries = showedInquiries.filter(
inquiry =>
inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
)
}
return showedInquiries
},
allInquiries() {
return this.predefinedInquiries.concat(this.inquiries)
},
processedInquiryIndex() {
return this.inquiries.findIndex(
inquiry => inquiry.id === this.processedInquiryId
)
},
deleteDialogMsg() {
if (
!this.deleteGroup &&
(this.processedInquiryIndex === null ||
this.processedInquiryIndex < 0 ||
this.processedInquiryIndex > this.inquiries.length)
) {
return ''
}
const deleteItem = this.deleteGroup
? `${this.selectedNotPredefinedCount} ${
this.selectedNotPredefinedCount > 1 ? 'inquiries' : 'inquiry'
}`
: `"${this.inquiries[this.processedInquiryIndex].name}"`
return `Are you sure you want to delete ${deleteItem}?`
}
},
watch: {
showedInquiries: {
handler() {
this.selectedInquiriesIds = new Set(
this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
if (this.$refs.mainCheckBox) {
this.$refs.mainCheckBox.checked = false
}
this.selectAll = false
}
},
deep: true
}
},
async created() {
const loadingPredefinedInquiries =
this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded =
this.$store.state.predefinedInquiriesLoaded
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
try {
this.$store.commit('setLoadingPredefinedInquiries', true)
const inquiries = await storedInquiries.readPredefinedInquiries()
this.$store.commit('updatePredefinedInquiries', inquiries)
this.$store.commit('setPredefinedInquiriesLoaded', true)
} catch (e) {
console.error(e)
}
this.$store.commit('setLoadingPredefinedInquiries', false)
}
},
mounted() {
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
this.resizeObserver.observe(this.$refs['my-inquiries-content'])
this.tableResizeObserver = new ResizeObserver(this.calcNameWidth)
this.tableResizeObserver.observe(this.$refs.table)
this.calcNameWidth()
this.calcMaxTableHeight()
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
this.tableResizeObserver.unobserve(this.$refs.table)
},
methods: {
emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
createdAtFormatted(value) {
if (!value) {
return ''
}
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' }
const timeOptions = {
hour12: false,
hour: '2-digit',
minute: '2-digit'
}
return (
new Date(value).toLocaleDateString('en-GB', dateOptions) +
' ' +
new Date(value).toLocaleTimeString('en-GB', timeOptions)
)
},
calcNameWidth() {
const nameWidth =
this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width
: 0
this.$refs['name-th'].style = `width: ${nameWidth}px`
},
calcMaxTableHeight() {
const freeSpace = this.$refs['my-inquiries-content'].offsetHeight - 200
this.maxTableHeight = freeSpace - (freeSpace % 40) + 1
},
openInquiry(index) {
const tab = this.showedInquiries[index]
setTimeout(() => {
this.$store.dispatch('addTab', tab).then(id => {
this.$store.commit('setCurrentTabId', id)
this.$router.push('/workspace')
})
})
},
showRenameDialog(id) {
this.errorMsg = null
this.processedInquiryId = id
this.newName = this.inquiries[this.processedInquiryIndex].name
this.$modal.show('rename')
},
renameInquiry() {
if (!this.newName) {
this.errorMsg = "Inquiry name can't be empty"
return
}
this.$store.dispatch('renameInquiry', {
inquiryId: this.processedInquiryId,
newName: this.newName
})
// hide dialog
this.$modal.hide('rename')
},
duplicateInquiry(index) {
const newInquiry = storedInquiries.duplicateInquiry(
this.showedInquiries[index]
)
this.$store.dispatch('addInquiry', newInquiry)
},
showDeleteDialog(idsSet) {
this.deleteGroup = idsSet.size > 1
if (!this.deleteGroup) {
this.processedInquiryId = idsSet.values().next().value
}
this.$modal.show('delete')
},
deleteInquiry() {
this.$modal.hide('delete')
if (!this.deleteGroup) {
this.$store.dispatch(
'deleteInquiries',
new Set().add(this.processedInquiryId)
)
// Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
this.selectedInquiriesIds.delete(this.processedInquiryId)
}
} else {
this.$store.dispatch('deleteInquiries', this.selectedInquiriesIds)
// Clear checkboxes
this.selectedInquiriesIds.clear()
}
this.selectedInquiriesCount = this.selectedInquiriesIds.size
},
exportToFile(inquiryList, fileName) {
storedInquiries.export(inquiryList, fileName)
},
exportSelectedInquiries() {
const inquiryList = this.allInquiries.filter(inquiry =>
this.selectedInquiriesIds.has(inquiry.id)
)
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
},
importInquiries() {
storedInquiries.importInquiries().then(importedInquiries => {
this.$store.commit(
'setInquiries',
this.inquiries.concat(importedInquiries)
)
})
},
toggleSelectAll(checked) {
this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => {
item.checked = checked
})
this.selectedInquiriesIds = checked
? new Set(this.showedInquiries.map(inquiry => inquiry.id))
: new Set()
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked
? [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
: 0
},
toggleRow(checked, id) {
const isPredefined = this.predefinedInquiriesIds.has(id)
if (checked) {
this.selectedInquiriesIds.add(id)
if (!isPredefined) {
this.selectedNotPredefinedCount += 1
}
} else {
if (this.selectedInquiriesIds.size === this.showedInquiries.length) {
this.$refs.mainCheckBox.checked = false
this.selectAll = false
}
this.selectedInquiriesIds.delete(id)
if (!isPredefined) {
this.selectedNotPredefinedCount -= 1
}
}
this.selectedInquiriesCount = this.selectedInquiriesIds.size
}
}
}
</script>
<style scoped>
#my-inquiries-container {
position: relative;
}
#loading-predefined-status {
position: absolute;
right: 0;
display: flex;
gap: 4px;
font-size: 12px;
color: var(--color-text-light-2);
align-items: center;
padding: 8px;
}
#start-guide {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 14px;
text-align: center;
}
#inquiries-not-found {
padding: 35px 5px;
border-radius: 5px;
border: 1px solid var(--color-border-light);
color: var(--color-text-base);
font-size: 14px;
text-align: center;
}
#my-inquiries-content {
padding: 52px;
height: 100%;
box-sizing: border-box;
}
#my-inquiries-toolbar {
display: flex;
justify-content: space-between;
margin-bottom: 18px;
margin: 0 auto 8px;
max-width: 1500px;
width: 100%;
}
.rounded-bg {
padding-top: 40px;
margin: 0 auto;
max-width: 1500px;
width: 100%;
}
.fixed-header {
padding: 11px 24px;
}
.fixed-header:first-child {
display: flex;
align-items: center;
padding-left: 12px;
}
.fixed-header:first-child .name-th {
margin-left: 24px;
}
table.sqliteviz-table {
margin-top: 0;
}
.sqliteviz-table tbody tr td {
min-width: 0;
height: 40px;
}
.sqliteviz-table tbody tr td:first-child {
width: 70%;
max-width: 0;
padding: 0 12px;
}
.sqliteviz-table tbody tr td:last-child {
width: 30%;
max-width: 0;
padding: 0 24px;
}
.sqliteviz-table tbody .cell-data {
display: flex;
align-items: center;
max-width: 100%;
width: 100%;
}
.sqliteviz-table tbody .cell-data div.name {
overflow: hidden;
text-overflow: ellipsis;
margin-left: 24px;
}
.sqliteviz-table tbody tr:hover td {
cursor: pointer;
}
.sqliteviz-table tbody tr:hover td {
color: var(--color-text-active);
}
.sqliteviz-table .second-column {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 100%;
}
.icons-container {
display: none;
margin-right: -12px;
}
.date-container {
flex-shrink: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.sqliteviz-table tbody tr:hover .icons-container {
display: flex;
}
.dialog input {
width: 100%;
}
button.toolbar {
margin-right: 16px;
}
.badge {
display: none;
background-color: var(--color-gray-light-4);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-small);
padding: 2px 6px;
font-size: 11px;
line-height: normal;
margin-left: 12px;
}
.sqliteviz-table tbody tr:hover .badge {
display: block;
}
#note {
margin-top: 24px;
}
#note img {
vertical-align: middle;
}
.icon-tooltip {
display: block;
width: 149px;
white-space: normal;
height: auto;
line-height: normal;
padding: 6px;
}
</style>