mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
18 Commits
559e04200c
...
0.27.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59c21c14e | ||
|
|
4ed4b54a28 | ||
|
|
2c2bb7d6d3 | ||
|
|
efbd985b36 | ||
|
|
9cf7d0e5dc | ||
|
|
0a8c09b58d | ||
|
|
931cf380bc | ||
|
|
f0f96ac663 | ||
|
|
45530cc9d6 | ||
|
|
6fbf75b601 | ||
|
|
d3fbf08569 | ||
|
|
be6a19a30f | ||
|
|
07d7a9d54b | ||
|
|
cdd925b8af | ||
|
|
12fa0749b1 | ||
|
|
75bf849823 | ||
|
|
3ee825defe | ||
|
|
77df3a8446 |
@@ -14,7 +14,9 @@ module.exports = {
|
||||
'vue/no-mutating-props': 'warn',
|
||||
'vue/no-reserved-component-names': 'warn',
|
||||
'vue/no-v-model-argument': 'off',
|
||||
'vue/require-default-prop': 'off'
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
||||
'vue/attribute-hyphenation': ['error', 'never']
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
@@ -21,8 +21,9 @@ jobs:
|
||||
- name: Install browsers
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo add-apt-repository -y ppa:mozillateam/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y chromium-browser firefox
|
||||
sudo apt-get install -y chromium-browser firefox-esr
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@10
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||
#
|
||||
|
||||
FROM node:12.22-buster
|
||||
FROM node:12.22-bullseye
|
||||
|
||||
RUN set -ex; \
|
||||
apt update; \
|
||||
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
from urllib import request
|
||||
|
||||
|
||||
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
|
||||
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
|
||||
|
||||
# Extension-functions
|
||||
# ===================
|
||||
@@ -22,15 +22,15 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
|
||||
extension_urls = (
|
||||
# Miscellaneous extensions
|
||||
# ========================
|
||||
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
|
||||
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
||||
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
|
||||
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
|
||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
||||
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
|
||||
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
||||
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
|
||||
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
|
||||
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
|
||||
# Third-party extension
|
||||
# =====================
|
||||
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||
# Third-party extension with own dependencies
|
||||
# ===========================================
|
||||
|
||||
2
lib/sql-js/dist/sql-wasm.js
vendored
2
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Binary file not shown.
24
package-lock.json
generated
24
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.25.1",
|
||||
"version": "0.27.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sqliteviz",
|
||||
"version": "0.25.1",
|
||||
"version": "0.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
@@ -3259,12 +3259,20 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
|
||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
|
||||
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
|
||||
"version": "17.0.85",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.85.tgz",
|
||||
"integrity": "sha512-5oBDUsRDsrYq4DdyHaL99gE1AJCfuDhyxqF6/55fvvOIRkp1PpKuwJ+aMiGJR+GJt7YqMNclPROTHF20vY2cXA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "^0.16",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
@@ -3276,6 +3284,12 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/scheduler": {
|
||||
"version": "0.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
|
||||
"integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/supercluster": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.26.0",
|
||||
"version": "0.27.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
<modals-container />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
import { ModalsContainer } from 'vue-final-modal'
|
||||
|
||||
export default {
|
||||
components: { ModalsContainer },
|
||||
computed: {
|
||||
inquiries() {
|
||||
return this.$store.state.inquiries
|
||||
@@ -26,6 +23,11 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||
addEventListener('storage', event => {
|
||||
if (event.key === storedInquiries.myInquiriesKey) {
|
||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<modal
|
||||
:modal-id="dialogName"
|
||||
:modalId="dialogName"
|
||||
class="dialog"
|
||||
content-class="import-modal"
|
||||
contentClass="import-modal"
|
||||
scrollable
|
||||
:click-to-close="false"
|
||||
:clickToClose="false"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
{{ typeName }} import
|
||||
@@ -17,7 +17,7 @@
|
||||
label="Table name"
|
||||
width="484px"
|
||||
:disabled="disableDialog"
|
||||
:error-msg="tableNameError"
|
||||
:errorMsg="tableNameError"
|
||||
/>
|
||||
<div v-if="!isJson && !isNdJson" class="chars">
|
||||
<delimiter-selector
|
||||
@@ -45,7 +45,7 @@
|
||||
The character used to escape the quote character within a field
|
||||
(e.g. "column with ""quotes"" in text").
|
||||
'
|
||||
max-hint-width="242px"
|
||||
maxHintWidth="242px"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="newDb"
|
||||
dialog-name="importFromCsvJson"
|
||||
dialogName="importFromCsvJson"
|
||||
@cancel="cancelImport"
|
||||
@finish="finish"
|
||||
/>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<modal
|
||||
:modal-id="name"
|
||||
v-model="show"
|
||||
class="dialog"
|
||||
:click-to-close="false"
|
||||
:content-transition="{ name: 'loading-dialog' }"
|
||||
:overlay-transition="{ name: 'loading-dialog' }"
|
||||
:clickToClose="false"
|
||||
:contentTransition="{ name: 'loading-dialog' }"
|
||||
:overlayTransition="{ name: 'loading-dialog' }"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
{{ title }}
|
||||
<close-icon :disabled="loading" @click="$emit('cancel')" />
|
||||
<close-icon :disabled="loading" @click="cancel" />
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-if="loading" class="loading-dialog-body">
|
||||
@@ -28,7 +29,7 @@
|
||||
class="secondary"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="$emit('cancel')"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -52,24 +53,33 @@ export default {
|
||||
name: 'LoadingDialog',
|
||||
components: { LoadingIndicator, CloseIcon },
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
loadingMsg: String,
|
||||
successMsg: String,
|
||||
actionBtnName: String,
|
||||
name: String,
|
||||
title: String,
|
||||
loading: Boolean
|
||||
},
|
||||
emits: ['cancel', 'action'],
|
||||
emits: ['cancel', 'action', 'update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
show: this.modelValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue() {
|
||||
this.show = this.modelValue
|
||||
},
|
||||
loading() {
|
||||
if (this.loading) {
|
||||
this.$modal.show(this.name)
|
||||
this.$emit('update:modelValue', true)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$emit('cancel')
|
||||
this.$emit('update:modelValue', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<template>
|
||||
<paginate
|
||||
v-model="page"
|
||||
:page-count="pageCount"
|
||||
:page-range="5"
|
||||
:margin-pages="1"
|
||||
:prev-text="chevron"
|
||||
:next-text="chevron"
|
||||
:no-li-surround="true"
|
||||
container-class="paginator-continer"
|
||||
page-link-class="paginator-page-link"
|
||||
active-class="paginator-active-page"
|
||||
break-view-link-class="paginator-break"
|
||||
next-link-class="paginator-next"
|
||||
prev-link-class="paginator-prev"
|
||||
disabled-class="paginator-disabled"
|
||||
:pageCount="pageCount"
|
||||
:pageRange="5"
|
||||
:marginPages="1"
|
||||
:prevText="chevron"
|
||||
:nextText="chevron"
|
||||
:noLiSurround="true"
|
||||
containerClass="paginator-continer"
|
||||
pageLinkClass="paginator-page-link"
|
||||
activeClass="paginator-active-page"
|
||||
breakViewLinkClass="paginator-break"
|
||||
nextLinkClass="paginator-next"
|
||||
prevLinkClass="paginator-prev"
|
||||
disabledClass="paginator-disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<pager
|
||||
v-show="pageCount > 1"
|
||||
v-model="currentPage"
|
||||
:page-count="pageCount"
|
||||
:pageCount="pageCount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
v-if="hint"
|
||||
class="hint"
|
||||
:hint="hint"
|
||||
:max-width="maxHintWidth || '149px'"
|
||||
:maxWidth="maxHintWidth || '149px'"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
|
||||
@@ -4,11 +4,13 @@ import events from '@/lib/utils/events'
|
||||
import migration from './_migrations'
|
||||
|
||||
const migrate = migration._migrate
|
||||
const myInquiriesKey = 'myInquiries'
|
||||
|
||||
export default {
|
||||
version: 2,
|
||||
myInquiriesKey,
|
||||
getStoredInquiries() {
|
||||
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
|
||||
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
|
||||
if (!myInquiries) {
|
||||
const oldInquiries = localStorage.getItem('myQueries')
|
||||
if (oldInquiries) {
|
||||
@@ -26,7 +28,8 @@ export default {
|
||||
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
||||
newInquiry.name = newInquiry.name + ' Copy'
|
||||
newInquiry.id = nanoid()
|
||||
newInquiry.createdAt = new Date()
|
||||
newInquiry.createdAt = new Date().toJSON()
|
||||
newInquiry.updatedAt = new Date().toJSON()
|
||||
delete newInquiry.isPredefined
|
||||
|
||||
return newInquiry
|
||||
@@ -38,7 +41,7 @@ export default {
|
||||
|
||||
updateStorage(inquiries) {
|
||||
localStorage.setItem(
|
||||
'myInquiries',
|
||||
myInquiriesKey,
|
||||
JSON.stringify({ version: this.version, inquiries })
|
||||
)
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export default class Tab {
|
||||
|
||||
this.isSaved = !!inquiry.id
|
||||
this.state = state
|
||||
this.updatedAt = inquiry.updatedAt
|
||||
}
|
||||
|
||||
async execute() {
|
||||
|
||||
@@ -7,7 +7,7 @@ import LoadView from '@/views/LoadView'
|
||||
import store from '@/store'
|
||||
import database from '@/lib/database'
|
||||
|
||||
const routes = [
|
||||
export const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Welcome',
|
||||
|
||||
@@ -17,28 +17,33 @@ export default {
|
||||
},
|
||||
async saveInquiry({ state }, { inquiryTab, newName }) {
|
||||
const value = {
|
||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||
id: inquiryTab.isPredefined || newName ? nanoid() : inquiryTab.id,
|
||||
query: inquiryTab.query,
|
||||
viewType: inquiryTab.dataView.mode,
|
||||
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||
name: newName || inquiryTab.name
|
||||
name: newName || inquiryTab.name,
|
||||
updatedAt: new Date().toJSON()
|
||||
}
|
||||
|
||||
// Get inquiries from local storage
|
||||
const myInquiries = state.inquiries
|
||||
|
||||
let inquiryIndex
|
||||
// Set createdAt
|
||||
if (newName) {
|
||||
value.createdAt = new Date()
|
||||
value.createdAt = new Date().toJSON()
|
||||
} else {
|
||||
var inquiryIndex = myInquiries.findIndex(
|
||||
inquiryIndex = myInquiries.findIndex(
|
||||
oldInquiry => oldInquiry.id === inquiryTab.id
|
||||
)
|
||||
value.createdAt = myInquiries[inquiryIndex].createdAt
|
||||
|
||||
value.createdAt =
|
||||
inquiryIndex !== -1
|
||||
? myInquiries[inquiryIndex].createdAt
|
||||
: new Date().toJSON()
|
||||
}
|
||||
|
||||
// Insert in inquiries list
|
||||
if (newName) {
|
||||
if (newName || inquiryIndex === -1) {
|
||||
myInquiries.push(value)
|
||||
} else {
|
||||
myInquiries.splice(inquiryIndex, 1, value)
|
||||
|
||||
@@ -7,7 +7,8 @@ export default {
|
||||
},
|
||||
|
||||
updateTab(state, { tab, newValues }) {
|
||||
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
||||
const { name, id, query, viewType, viewOptions, isSaved, updatedAt } =
|
||||
newValues
|
||||
const oldId = tab.id
|
||||
|
||||
if (id && state.currentTabId === oldId) {
|
||||
@@ -36,6 +37,9 @@ export default {
|
||||
// Saved inquiry is not predefined
|
||||
delete tab.isPredefined
|
||||
}
|
||||
if (updatedAt) {
|
||||
tab.updatedAt = updatedAt
|
||||
}
|
||||
},
|
||||
|
||||
deleteTab(state, tab) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
src="~@/assets/images/info.svg"
|
||||
@click="$modal.show('app-info')"
|
||||
/>
|
||||
<modal modal-id="app-info" class="dialog" content-class="app-info-modal">
|
||||
<modal modalId="app-info" class="dialog" contentClass="app-info-modal">
|
||||
<div class="dialog-header">
|
||||
App info
|
||||
<close-icon @click="$modal.hide('app-info')" />
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
<copy-icon @click="duplicateInquiry(index)" />
|
||||
<export-icon
|
||||
tooltip="Export inquiry to file"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
|
||||
/>
|
||||
<delete-icon
|
||||
@@ -140,7 +140,7 @@
|
||||
</div>
|
||||
|
||||
<!--Rename Inquiry dialog -->
|
||||
<modal modal-id="rename" class="dialog" content-style="width: 560px;">
|
||||
<modal modalId="rename" class="dialog" contentStyle="width: 560px;">
|
||||
<div class="dialog-header">
|
||||
Rename inquiry
|
||||
<close-icon @click="$modal.hide('rename')" />
|
||||
@@ -149,7 +149,7 @@
|
||||
<text-field
|
||||
v-model="newName"
|
||||
label="New inquiry name"
|
||||
:error-msg="errorMsg"
|
||||
:errorMsg="errorMsg"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
</modal>
|
||||
|
||||
<!--Delete Inquiry dialog -->
|
||||
<modal modal-id="delete" class="dialog" content-style="width: 480px;">
|
||||
<modal modalId="delete" class="dialog" contentStyle="width: 480px;">
|
||||
<div class="dialog-header">
|
||||
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
|
||||
<close-icon @click="$modal.hide('delete')" />
|
||||
|
||||
@@ -10,14 +10,22 @@
|
||||
</div>
|
||||
<div id="nav-buttons">
|
||||
<button
|
||||
v-show="currentInquiry && $route.path === '/workspace'"
|
||||
v-show="currentInquiryTab && $route.path === '/workspace'"
|
||||
id="save-btn"
|
||||
class="primary"
|
||||
:disabled="isSaved"
|
||||
@click="checkInquiryBeforeSave"
|
||||
@click="onSave(false)"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
v-show="currentInquiryTab && $route.path === '/workspace'"
|
||||
id="save-as-btn"
|
||||
class="primary"
|
||||
@click="onSaveAs"
|
||||
>
|
||||
Save as
|
||||
</button>
|
||||
<button id="create-btn" class="primary" @click="createNewInquiry">
|
||||
Create
|
||||
</button>
|
||||
@@ -25,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<!--Save Inquiry dialog -->
|
||||
<modal modal-id="save" class="dialog" content-style="width: 560px;">
|
||||
<modal modalId="save" class="dialog" contentStyle="width: 560px;">
|
||||
<div class="dialog-header">
|
||||
Save inquiry
|
||||
<close-icon @click="cancelSave" />
|
||||
@@ -39,13 +47,40 @@
|
||||
<text-field
|
||||
v-model="name"
|
||||
label="Inquiry name"
|
||||
:error-msg="errorMsg"
|
||||
:errorMsg="errorMsg"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||
<button class="primary" @click="saveInquiry">Save</button>
|
||||
<button class="primary" @click="validateSaveFormAndSaveInquiry">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
<!-- Inquiery saving conflict dialog -->
|
||||
<modal
|
||||
modalId="inquiry-conflict"
|
||||
class="dialog"
|
||||
contentStyle="width: 560px;"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
Inquiry saving conflict
|
||||
<close-icon @click="cancelSave" />
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div id="save-note">
|
||||
<img src="~@/assets/images/info.svg" />
|
||||
This inquiry has been modified in the mean time. This can happen if an
|
||||
inquiry is saved in another window or browser tab. Do you want to
|
||||
overwrite that changes or save the current state as a new inquiry?
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||
<button class="primary" @click="onSave(true)">Overwrite</button>
|
||||
<button class="primary" @click="onSaveAs">Save as new</button>
|
||||
</div>
|
||||
</modal>
|
||||
</nav>
|
||||
@@ -73,25 +108,28 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentInquiry() {
|
||||
inquiries() {
|
||||
return this.$store.state.inquiries
|
||||
},
|
||||
currentInquiryTab() {
|
||||
return this.$store.state.currentTab
|
||||
},
|
||||
isSaved() {
|
||||
return this.currentInquiry && this.currentInquiry.isSaved
|
||||
return this.currentInquiryTab && this.currentInquiryTab.isSaved
|
||||
},
|
||||
isPredefined() {
|
||||
return this.currentInquiry && this.currentInquiry.isPredefined
|
||||
return this.currentInquiryTab && this.currentInquiryTab.isPredefined
|
||||
},
|
||||
runDisabled() {
|
||||
return (
|
||||
this.currentInquiry &&
|
||||
(!this.$store.state.db || !this.currentInquiry.query)
|
||||
this.currentInquiryTab &&
|
||||
(!this.$store.state.db || !this.currentInquiryTab.query)
|
||||
)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
eventBus.$on('createNewInquiry', this.createNewInquiry)
|
||||
eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
|
||||
eventBus.$on('saveInquiry', this.onSave)
|
||||
document.addEventListener('keydown', this._keyListener)
|
||||
},
|
||||
beforeUnmount() {
|
||||
@@ -109,63 +147,84 @@ export default {
|
||||
events.send('inquiry.create', null, { auto: false })
|
||||
},
|
||||
cancelSave() {
|
||||
this.$modal.hide('save')
|
||||
eventBus.$off('inquirySaved')
|
||||
},
|
||||
checkInquiryBeforeSave() {
|
||||
this.errorMsg = null
|
||||
this.name = ''
|
||||
|
||||
if (storedInquiries.isTabNeedName(this.currentInquiry)) {
|
||||
this.$modal.show('save')
|
||||
} else {
|
||||
this.saveInquiry()
|
||||
}
|
||||
this.$modal.hide('save')
|
||||
this.$modal.hide('inquiry-conflict')
|
||||
eventBus.$off('inquirySaved')
|
||||
},
|
||||
async saveInquiry() {
|
||||
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
|
||||
if (isNeedName && !this.name) {
|
||||
onSave(skipConcurrentEditingCheck = false) {
|
||||
if (storedInquiries.isTabNeedName(this.currentInquiryTab)) {
|
||||
this.openSaveModal()
|
||||
return
|
||||
}
|
||||
|
||||
if (!skipConcurrentEditingCheck) {
|
||||
const inquiryInStore = this.inquiries.find(
|
||||
inquiry => inquiry.id === this.currentInquiryTab.id
|
||||
)
|
||||
|
||||
if (
|
||||
inquiryInStore &&
|
||||
inquiryInStore.updatedAt !== this.currentInquiryTab.updatedAt
|
||||
) {
|
||||
this.$modal.show('inquiry-conflict')
|
||||
return
|
||||
}
|
||||
}
|
||||
this.saveInquiry()
|
||||
},
|
||||
onSaveAs() {
|
||||
this.openSaveModal()
|
||||
},
|
||||
openSaveModal() {
|
||||
this.$modal.hide('inquiry-conflict')
|
||||
this.errorMsg = null
|
||||
this.name = ''
|
||||
this.$modal.show('save')
|
||||
},
|
||||
validateSaveFormAndSaveInquiry() {
|
||||
if (!this.name) {
|
||||
this.errorMsg = "Inquiry name can't be empty"
|
||||
return
|
||||
}
|
||||
const dataSet = this.currentInquiry.result
|
||||
const tabView = this.currentInquiry.view
|
||||
this.saveInquiry()
|
||||
},
|
||||
async saveInquiry() {
|
||||
const eventName =
|
||||
this.currentInquiryTab.name && this.name
|
||||
? 'inquiry.saveAs'
|
||||
: 'inquiry.save'
|
||||
|
||||
// Save inquiry
|
||||
const value = await this.$store.dispatch('saveInquiry', {
|
||||
inquiryTab: this.currentInquiry,
|
||||
inquiryTab: this.currentInquiryTab,
|
||||
newName: this.name
|
||||
})
|
||||
|
||||
// Update tab in store
|
||||
this.$store.commit('updateTab', {
|
||||
tab: this.currentInquiry,
|
||||
tab: this.currentInquiryTab,
|
||||
newValues: {
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
query: value.query,
|
||||
viewType: value.viewType,
|
||||
viewOptions: value.viewOptions,
|
||||
isSaved: true
|
||||
isSaved: true,
|
||||
updatedAt: value.updatedAt
|
||||
}
|
||||
})
|
||||
|
||||
// Restore data:
|
||||
// e.g. if we save predefined inquiry the tab will be created again
|
||||
// (because of new id) and
|
||||
// it will be without sql result and has default view - table.
|
||||
// That's why we need to restore data and view
|
||||
this.$nextTick(() => {
|
||||
this.currentInquiry.result = dataSet
|
||||
this.currentInquiry.view = tabView
|
||||
})
|
||||
|
||||
// Hide dialog
|
||||
// Hide dialogs
|
||||
this.$modal.hide('save')
|
||||
this.$modal.hide('inquiry-conflict')
|
||||
this.errorMsg = null
|
||||
this.name = ''
|
||||
|
||||
// Signal about saving
|
||||
eventBus.$emit('inquirySaved')
|
||||
events.send('inquiry.save')
|
||||
events.send(eventName)
|
||||
},
|
||||
_keyListener(e) {
|
||||
if (this.$route.path === '/workspace') {
|
||||
@@ -173,19 +232,25 @@ export default {
|
||||
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (!this.runDisabled) {
|
||||
this.currentInquiry.execute()
|
||||
this.currentInquiryTab.execute()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Save inquiry Ctrl+S
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
if (!this.isSaved) {
|
||||
this.checkInquiryBeforeSave()
|
||||
this.onSave()
|
||||
}
|
||||
return
|
||||
}
|
||||
// Save inquiry as Ctrl+Shift+S
|
||||
if (e.key === 'S' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
||||
e.preventDefault()
|
||||
this.onSaveAs()
|
||||
return
|
||||
}
|
||||
}
|
||||
// New (blank) inquiry Ctrl+B
|
||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="$store.state.db"
|
||||
dialog-name="addCsvJson"
|
||||
dialogName="addCsvJson"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="chartContainer" class="chart-container">
|
||||
<div v-show="!dataSources && visible" class="warning chart-warning">
|
||||
<div v-show="!dataSources" class="warning chart-warning">
|
||||
There is no data to build a chart. Run your SQL query and make sure the
|
||||
result is not empty.
|
||||
</div>
|
||||
@@ -9,18 +9,17 @@
|
||||
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
|
||||
>
|
||||
<PlotlyEditor
|
||||
v-show="visible"
|
||||
ref="plotlyEditor"
|
||||
:data="state.data"
|
||||
:layout="state.layout"
|
||||
:frames="state.frames"
|
||||
:config="config"
|
||||
:data-sources="dataSources"
|
||||
:data-source-options="dataSourceOptions"
|
||||
:dataSources="dataSources"
|
||||
:dataSourceOptions="dataSourceOptions"
|
||||
:plotly="plotly"
|
||||
:use-resize-handler="useResizeHandler"
|
||||
:useResizeHandler="useResizeHandler"
|
||||
:debug="true"
|
||||
:advanced-trace-type-selector="true"
|
||||
:advancedTraceTypeSelector="true"
|
||||
@update="update"
|
||||
@render="onRender"
|
||||
/>
|
||||
@@ -64,7 +63,6 @@ export default {
|
||||
displaylogo: false,
|
||||
modeBarButtonsToRemove: ['toImage']
|
||||
},
|
||||
visible: true,
|
||||
resizeObserver: null,
|
||||
useResizeHandler: this.$store.state.isWorkspaceVisible
|
||||
}
|
||||
@@ -137,7 +135,7 @@ export default {
|
||||
updatePlotly() {
|
||||
const plotComponent = this.$refs.plotlyEditor.plotComponentRef.current
|
||||
plotComponent.updatePlotly(
|
||||
false, // shouldInvokeResizeHandler
|
||||
true, // shouldInvokeResizeHandler
|
||||
plotComponent.props.onUpdate, // figureCallbackFunction
|
||||
false // shouldAttachUpdateEvents
|
||||
)
|
||||
@@ -163,11 +161,8 @@ export default {
|
||||
'text/html'
|
||||
)
|
||||
},
|
||||
async prepareCopy(type = 'png') {
|
||||
return await chartHelper.getImageDataUrl(
|
||||
this.$refs.plotlyEditor.$el,
|
||||
type
|
||||
)
|
||||
prepareCopy(type = 'png') {
|
||||
return chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
:options="colsToSelect"
|
||||
:disabled="colsToSelect.length === 0"
|
||||
:multiple="true"
|
||||
:hide-selected="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:hideSelected="true"
|
||||
:closeOnSelect="true"
|
||||
:showLabels="false"
|
||||
:max="colsToSelect.length"
|
||||
open-direction="bottom"
|
||||
openDirection="bottom"
|
||||
placeholder=""
|
||||
>
|
||||
<template #maxElements>
|
||||
@@ -37,12 +37,12 @@
|
||||
:options="rowsToSelect"
|
||||
:disabled="rowsToSelect.length === 0"
|
||||
:multiple="true"
|
||||
:hide-selected="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:hideSelected="true"
|
||||
:closeOnSelect="true"
|
||||
:showLabels="false"
|
||||
:max="rowsToSelect.length"
|
||||
:option-height="29"
|
||||
open-direction="bottom"
|
||||
:optionHeight="29"
|
||||
openDirection="bottom"
|
||||
placeholder=""
|
||||
>
|
||||
<template #maxElements>
|
||||
@@ -65,12 +65,12 @@
|
||||
class="sqliteviz-select short aggregator"
|
||||
:options="aggregators"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:hide-selected="true"
|
||||
:option-height="29"
|
||||
open-direction="bottom"
|
||||
trackBy="name"
|
||||
:closeOnSelect="true"
|
||||
:showLabels="false"
|
||||
:hideSelected="true"
|
||||
:optionHeight="29"
|
||||
openDirection="bottom"
|
||||
placeholder="Choose a function"
|
||||
>
|
||||
<template #noResult>
|
||||
@@ -84,11 +84,11 @@
|
||||
class="sqliteviz-select aggr-arg"
|
||||
:options="keyNames"
|
||||
:disabled="keyNames.length === 0"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:hide-selected="true"
|
||||
:option-height="29"
|
||||
open-direction="bottom"
|
||||
:closeOnSelect="true"
|
||||
:showLabels="false"
|
||||
:hideSelected="true"
|
||||
:optionHeight="29"
|
||||
openDirection="bottom"
|
||||
placeholder="Choose an argument"
|
||||
/>
|
||||
|
||||
@@ -98,11 +98,11 @@
|
||||
class="sqliteviz-select aggr-arg"
|
||||
:options="keyNames"
|
||||
:disabled="keyNames.length === 0"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:hide-selected="true"
|
||||
:option-height="29"
|
||||
open-direction="bottom"
|
||||
:closeOnSelect="true"
|
||||
:showLabels="false"
|
||||
:hideSelected="true"
|
||||
:optionHeight="29"
|
||||
openDirection="bottom"
|
||||
placeholder="Choose a second argument"
|
||||
/>
|
||||
</div>
|
||||
@@ -114,13 +114,13 @@
|
||||
class="sqliteviz-select short renderer"
|
||||
:options="renderers"
|
||||
label="name"
|
||||
track-by="name"
|
||||
:close-on-select="true"
|
||||
:allow-empty="false"
|
||||
:show-labels="false"
|
||||
:hide-selected="true"
|
||||
:option-height="29"
|
||||
open-direction="bottom"
|
||||
trackBy="name"
|
||||
:closeOnSelect="true"
|
||||
:allowEmpty="false"
|
||||
:showLabels="false"
|
||||
:hideSelected="true"
|
||||
:optionHeight="29"
|
||||
openDirection="bottom"
|
||||
placeholder="Choose a view"
|
||||
>
|
||||
<template #noResult>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
<pivot-ui
|
||||
v-model="pivotOptions"
|
||||
:key-names="columns"
|
||||
:keyNames="columns"
|
||||
@update="$emit('update')"
|
||||
/>
|
||||
<div ref="pivotOutput" class="pivot-output" />
|
||||
@@ -35,6 +35,7 @@ import pivotHelper from './pivotHelper'
|
||||
import Chart from '@/views/MainView/Workspace/Tabs/Tab/DataView/Chart'
|
||||
import chartHelper from '@/lib/chartHelper'
|
||||
import events from '@/lib/utils/events'
|
||||
import plotly from 'plotly.js'
|
||||
|
||||
export default {
|
||||
name: 'Pivot',
|
||||
@@ -130,17 +131,19 @@ export default {
|
||||
// We need to detect resizing because plotly doesn't resize when resize its container
|
||||
// but it resize on window.resize (we will trigger it manualy in order to make plotly resize)
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
this.resizeObserver.observe(this.$refs.customChartOutput)
|
||||
this.resizeObserver.observe(this.$refs.pivotOutput)
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resizeObserver.unobserve(this.$refs.customChartOutput)
|
||||
this.resizeObserver.unobserve(this.$refs.pivotOutput)
|
||||
},
|
||||
methods: {
|
||||
handleResize() {
|
||||
// hack: plotly changes size only on window.resize event,
|
||||
// so, we trigger it when container resizes (e.g. when move splitter)
|
||||
// so, we resize it manually when container resizes (e.g. when move splitter)
|
||||
if (this.viewStandartChart) {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
plotly.Plots.resize(
|
||||
this.$refs.pivotOutput.querySelector('.js-plotly-plot')
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -184,9 +187,7 @@ export default {
|
||||
)
|
||||
|
||||
// fix for Firefox: fit plotly renderers just after choosing it in pivotUi
|
||||
if (this.viewStandartChart) {
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}
|
||||
this.handleResize()
|
||||
},
|
||||
|
||||
getOptionsForSave() {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<component
|
||||
:is="mode"
|
||||
ref="viewComponent"
|
||||
v-model:import-to-png-enabled="importToPngEnabled"
|
||||
v-model:import-to-svg-enabled="importToSvgEnabled"
|
||||
:init-options="mode === initMode ? initOptions : undefined"
|
||||
v-model:importToPngEnabled="importToPngEnabled"
|
||||
v-model:importToSvgEnabled="importToSvgEnabled"
|
||||
:initOptions="mode === initMode ? initOptions : undefined"
|
||||
:data-sources="dataSource"
|
||||
@loading-image-completed="loadingImage = false"
|
||||
@update="$emit('update')"
|
||||
@@ -16,7 +16,7 @@
|
||||
<icon-button
|
||||
:active="mode === 'chart'"
|
||||
tooltip="Switch to chart"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="mode = 'chart'"
|
||||
>
|
||||
<chart-icon />
|
||||
@@ -25,7 +25,7 @@
|
||||
ref="pivotBtn"
|
||||
:active="mode === 'pivot'"
|
||||
tooltip="Switch to pivot"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="mode = 'pivot'"
|
||||
>
|
||||
<pivot-icon />
|
||||
@@ -37,7 +37,7 @@
|
||||
:disabled="!importToPngEnabled || loadingImage"
|
||||
:loading="loadingImage"
|
||||
tooltip="Save as PNG image"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="saveAsPng"
|
||||
>
|
||||
<png-icon />
|
||||
@@ -46,7 +46,7 @@
|
||||
ref="svgExportBtn"
|
||||
:disabled="!importToSvgEnabled"
|
||||
tooltip="Save as SVG"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="saveAsSvg"
|
||||
>
|
||||
<export-to-svg-icon />
|
||||
@@ -55,7 +55,7 @@
|
||||
<icon-button
|
||||
ref="htmlExportBtn"
|
||||
tooltip="Save as HTML"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="saveAsHtml"
|
||||
>
|
||||
<HtmlIcon />
|
||||
@@ -64,7 +64,7 @@
|
||||
ref="copyToClipboardBtn"
|
||||
:loading="copyingImage"
|
||||
tooltip="Copy visualisation to clipboard"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="prepareCopy"
|
||||
>
|
||||
<clipboard-icon />
|
||||
@@ -72,10 +72,10 @@
|
||||
</side-tool-bar>
|
||||
|
||||
<loading-dialog
|
||||
loading-msg="Rendering the visualisation..."
|
||||
success-msg="Image is ready"
|
||||
action-btn-name="Copy"
|
||||
name="prepareCopy"
|
||||
v-model="showLoadingDialog"
|
||||
loadingMsg="Rendering the visualisation..."
|
||||
successMsg="Image is ready"
|
||||
actionBtnName="Copy"
|
||||
title="Copy to clipboard"
|
||||
:loading="preparingCopy"
|
||||
@action="copyToClipboard"
|
||||
@@ -85,8 +85,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Chart from './Chart'
|
||||
import Pivot from './Pivot'
|
||||
import Chart from './Chart/index.vue'
|
||||
import Pivot from './Pivot/index.vue'
|
||||
import SideToolBar from '../SideToolBar'
|
||||
import IconButton from '@/components/IconButton'
|
||||
import ChartIcon from '@/components/svg/chart'
|
||||
@@ -96,7 +96,7 @@ import ExportToSvgIcon from '@/components/svg/exportToSvg'
|
||||
import PngIcon from '@/components/svg/png'
|
||||
import ClipboardIcon from '@/components/svg/clipboard'
|
||||
import cIo from '@/lib/utils/clipboardIo'
|
||||
import loadingDialog from '@/components/LoadingDialog'
|
||||
import loadingDialog from '@/components/LoadingDialog.vue'
|
||||
import time from '@/lib/utils/time'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
@@ -129,7 +129,8 @@ export default {
|
||||
loadingImage: false,
|
||||
copyingImage: false,
|
||||
preparingCopy: false,
|
||||
dataToCopy: null
|
||||
dataToCopy: null,
|
||||
showLoadingDialog: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -170,14 +171,13 @@ export default {
|
||||
async prepareCopy() {
|
||||
if ('ClipboardItem' in window) {
|
||||
this.preparingCopy = true
|
||||
this.$modal.show('prepareCopy')
|
||||
this.showLoadingDialog = true
|
||||
const t0 = performance.now()
|
||||
|
||||
await time.sleep(0)
|
||||
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
|
||||
const t1 = performance.now()
|
||||
if (t1 - t0 < 950) {
|
||||
this.$modal.hide('prepareCopy')
|
||||
this.copyToClipboard()
|
||||
} else {
|
||||
this.preparingCopy = false
|
||||
@@ -190,14 +190,13 @@ export default {
|
||||
)
|
||||
}
|
||||
},
|
||||
async copyToClipboard() {
|
||||
copyToClipboard() {
|
||||
cIo.copyImage(this.dataToCopy)
|
||||
this.$modal.hide('prepareCopy')
|
||||
this.showLoadingDialog = false
|
||||
this.exportSignal('clipboard')
|
||||
},
|
||||
cancelCopy() {
|
||||
this.dataToCopy = null
|
||||
this.$modal.hide('prepareCopy')
|
||||
},
|
||||
|
||||
saveAsSvg() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<icon-button
|
||||
:disabled="modelValue === 0"
|
||||
tooltip="First row"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
class="first"
|
||||
@click="$emit('update:modelValue', 0)"
|
||||
>
|
||||
@@ -12,7 +12,7 @@
|
||||
<icon-button
|
||||
:disabled="modelValue === 0"
|
||||
tooltip="Previous row"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
class="prev"
|
||||
@click="$emit('update:modelValue', modelValue - 1)"
|
||||
>
|
||||
@@ -21,7 +21,7 @@
|
||||
<icon-button
|
||||
:disabled="modelValue === total - 1"
|
||||
tooltip="Next row"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
class="next"
|
||||
@click="$emit('update:modelValue', modelValue + 1)"
|
||||
>
|
||||
@@ -30,7 +30,7 @@
|
||||
<icon-button
|
||||
:disabled="modelValue === total - 1"
|
||||
tooltip="Last row"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
class="last"
|
||||
@click="$emit('update:modelValue', total - 1)"
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<div class="value-viewer-container">
|
||||
<value-viewer
|
||||
v-show="selectedCell"
|
||||
:cell-value="
|
||||
:cellValue="
|
||||
selectedCell
|
||||
? result.values[result.columns[selectedCell.dataset.col]][
|
||||
selectedCell.dataset.row
|
||||
@@ -40,7 +40,7 @@
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
tooltip="Export result set to CSV file"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="exportToCsv"
|
||||
>
|
||||
<export-to-csv-icon />
|
||||
@@ -50,7 +50,7 @@
|
||||
ref="copyToClipboardBtn"
|
||||
:disabled="!result"
|
||||
tooltip="Copy result set to clipboard"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="prepareCopy"
|
||||
>
|
||||
<clipboard-icon />
|
||||
@@ -60,7 +60,7 @@
|
||||
ref="rowBtn"
|
||||
:disabled="!result"
|
||||
tooltip="View record"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
:active="viewRecord"
|
||||
@click="toggleViewRecord"
|
||||
>
|
||||
@@ -71,7 +71,7 @@
|
||||
ref="viewCellValueBtn"
|
||||
:disabled="!result"
|
||||
tooltip="View value"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
:active="viewValuePanelVisible"
|
||||
@click="toggleViewValuePanel"
|
||||
>
|
||||
@@ -80,10 +80,10 @@
|
||||
</side-tool-bar>
|
||||
|
||||
<loading-dialog
|
||||
loading-msg="Building CSV..."
|
||||
success-msg="CSV is ready"
|
||||
action-btn-name="Copy"
|
||||
name="prepareCSVCopy"
|
||||
v-model="showLoadingDialog"
|
||||
loadingMsg="Building CSV..."
|
||||
successMsg="CSV is ready"
|
||||
actionBtnName="Copy"
|
||||
title="Copy to clipboard"
|
||||
:loading="preparingCopy"
|
||||
@action="copyToClipboard"
|
||||
@@ -113,9 +113,9 @@
|
||||
v-if="result && !viewRecord"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:page-size="pageSize"
|
||||
:pageSize="pageSize"
|
||||
:page="defaultPage"
|
||||
:selected-cell-coordinates="defaultSelectedCell"
|
||||
:selectedCellCoordinates="defaultSelectedCell"
|
||||
class="straight"
|
||||
@update-selected-cell="onUpdateSelectedCell"
|
||||
/>
|
||||
@@ -125,8 +125,8 @@
|
||||
ref="recordView"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:selected-column-index="selectedCell ? +selectedCell.dataset.col : 0"
|
||||
:row-index="selectedCell ? +selectedCell.dataset.row : 0"
|
||||
:selectedColumnIndex="selectedCell ? +selectedCell.dataset.col : 0"
|
||||
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
|
||||
@update-selected-cell="onUpdateSelectedCell"
|
||||
/>
|
||||
</div>
|
||||
@@ -190,7 +190,8 @@ export default {
|
||||
viewRecord: false,
|
||||
defaultPage: 1,
|
||||
defaultSelectedCell: null,
|
||||
enableTeleport: this.$store.state.isWorkspaceVisible
|
||||
enableTeleport: this.$store.state.isWorkspaceVisible,
|
||||
showLoadingDialog: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -264,14 +265,13 @@ export default {
|
||||
|
||||
if ('ClipboardItem' in window) {
|
||||
this.preparingCopy = true
|
||||
this.$modal.show('prepareCSVCopy')
|
||||
this.showLoadingDialog = true
|
||||
const t0 = performance.now()
|
||||
|
||||
await time.sleep(0)
|
||||
this.dataToCopy = csv.serialize(this.result)
|
||||
const t1 = performance.now()
|
||||
if (t1 - t0 < 950) {
|
||||
this.$modal.hide('prepareCSVCopy')
|
||||
this.copyToClipboard()
|
||||
} else {
|
||||
this.preparingCopy = false
|
||||
@@ -287,12 +287,11 @@ export default {
|
||||
|
||||
copyToClipboard() {
|
||||
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||
this.$modal.hide('prepareCSVCopy')
|
||||
this.showLoadingDialog = false
|
||||
},
|
||||
|
||||
cancelCopy() {
|
||||
this.dataToCopy = null
|
||||
this.$modal.hide('prepareCSVCopy')
|
||||
},
|
||||
|
||||
toggleViewValuePanel() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
ref="sqlEditorBtn"
|
||||
:active="panel === 'sqlEditor'"
|
||||
tooltip="Switch panel to SQL editor"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="$emit('switchTo', 'sqlEditor')"
|
||||
>
|
||||
<sql-editor-icon />
|
||||
@@ -14,7 +14,7 @@
|
||||
ref="tableBtn"
|
||||
:active="panel === 'table'"
|
||||
tooltip="Switch panel to result set"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="$emit('switchTo', 'table')"
|
||||
>
|
||||
<table-icon />
|
||||
@@ -24,7 +24,7 @@
|
||||
ref="dataViewBtn"
|
||||
:active="panel === 'dataView'"
|
||||
tooltip="Switch panel to data view"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="$emit('switchTo', 'dataView')"
|
||||
>
|
||||
<data-view-icon />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
ref="cm"
|
||||
v-model:value="query"
|
||||
:options="cmOptions"
|
||||
:original-style="true"
|
||||
:originalStyle="true"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@
|
||||
:disabled="runDisabled"
|
||||
:loading="isGettingResults"
|
||||
tooltip="Run SQL query"
|
||||
tooltip-position="top-left"
|
||||
tooltipPosition="top-left"
|
||||
@click="$emit('run')"
|
||||
>
|
||||
<run-icon :disabled="runDisabled" />
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<sql-editor
|
||||
ref="sqlEditor"
|
||||
v-model="tab.query"
|
||||
:is-getting-results="tab.isGettingResults"
|
||||
:isGettingResults="tab.isGettingResults"
|
||||
@switch-to="onSwitchView('sqlEditor', $event)"
|
||||
@run="tab.execute()"
|
||||
/>
|
||||
@@ -39,7 +39,7 @@
|
||||
<run-result
|
||||
:tab="tab"
|
||||
:result="tab.result"
|
||||
:is-getting-results="tab.isGettingResults"
|
||||
:isGettingResults="tab.isGettingResults"
|
||||
:error="tab.error"
|
||||
:time="tab.time"
|
||||
@switch-to="onSwitchView('table', $event)"
|
||||
@@ -54,8 +54,8 @@
|
||||
<data-view
|
||||
ref="dataView"
|
||||
:data-source="(tab.result && tab.result.values) || null"
|
||||
:init-options="tab.viewOptions"
|
||||
:init-mode="tab.viewType"
|
||||
:initOptions="tab.viewOptions"
|
||||
:initMode="tab.viewType"
|
||||
@switch-to="onSwitchView('dataView', $event)"
|
||||
@update="onDataViewUpdate"
|
||||
/>
|
||||
@@ -134,7 +134,6 @@ export default {
|
||||
const fromPosition = this.tab.layout[from]
|
||||
this.tab.layout[from] = this.tab.layout[to]
|
||||
this.tab.layout[to] = fromPosition
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
events.send('inquiry.panel', null, { panel: to })
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<!--Close tab warning dialog -->
|
||||
<modal modal-id="close-warn" class="dialog" content-style="width: 560px;">
|
||||
<modal modalId="close-warn" class="dialog" contentStyle="width: 560px;">
|
||||
<div class="dialog-header">
|
||||
Close tab
|
||||
{{
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import { createStore } from 'vuex'
|
||||
import App from '@/App'
|
||||
import App from '@/App.vue'
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
import actions from '@/store/actions'
|
||||
import mutations from '@/store/mutations'
|
||||
import { nextTick } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { routes } from '@/router'
|
||||
|
||||
describe('App.vue', () => {
|
||||
let clock
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
@@ -59,4 +68,167 @@ describe('App.vue', () => {
|
||||
{ id: 3, name: 'bar' }
|
||||
])
|
||||
})
|
||||
|
||||
it('Updates store when inquirires change in local storage', async () => {
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 2, name: 'baz' },
|
||||
{ id: 3, name: 'bar' }
|
||||
])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: []
|
||||
}
|
||||
const store = createStore({ state, mutations })
|
||||
shallowMount(App, {
|
||||
global: { stubs: ['router-view'], plugins: [store] }
|
||||
})
|
||||
|
||||
expect(state.inquiries).to.eql([
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 2, name: 'baz' },
|
||||
{ id: 3, name: 'bar' }
|
||||
])
|
||||
|
||||
storedInquiries.getStoredInquiries.returns([
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 3, name: 'bar' }
|
||||
])
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'myInquiries' }))
|
||||
expect(state.inquiries).to.eql([
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 3, name: 'bar' }
|
||||
])
|
||||
})
|
||||
|
||||
it('Closes with saving and does not change the next tab', async () => {
|
||||
const inquiries = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: {},
|
||||
createdAt: '2020-11-07T20:57:04.492Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'bar',
|
||||
query: 'SELECT * FROM bar',
|
||||
viewType: 'chart',
|
||||
viewOptions: {},
|
||||
createdAt: '2020-11-07T20:57:04.492Z'
|
||||
}
|
||||
]
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns(inquiries)
|
||||
const tab1 = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: {},
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'bottom',
|
||||
dataView: 'hidden'
|
||||
},
|
||||
result: {
|
||||
columns: ['name', 'points'],
|
||||
values: {
|
||||
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
|
||||
points: [100, 90, 95, 80]
|
||||
}
|
||||
},
|
||||
isSaved: false
|
||||
}
|
||||
const tab2 = {
|
||||
id: 2,
|
||||
name: 'bar',
|
||||
query: 'SELECT * FROM bar',
|
||||
viewType: 'chart',
|
||||
viewOptions: {},
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'hidden',
|
||||
dataView: 'bottom'
|
||||
},
|
||||
result: {
|
||||
columns: ['id'],
|
||||
values: {
|
||||
id: [1, 2, 3]
|
||||
}
|
||||
},
|
||||
isSaved: true
|
||||
}
|
||||
// mock store state
|
||||
const state = {
|
||||
tabs: [tab1, tab2],
|
||||
currentTabId: 1,
|
||||
currentTab: tab1,
|
||||
db: {},
|
||||
inquiries
|
||||
}
|
||||
|
||||
const store = createStore({ state, mutations, actions })
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes
|
||||
})
|
||||
router.push('/workspace')
|
||||
|
||||
// After this line, router is ready
|
||||
await router.isReady()
|
||||
|
||||
const wrapper = mount(App, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
teleport: true,
|
||||
transition: false,
|
||||
schema: true,
|
||||
AppDiagnosticInfo: true,
|
||||
DataView: {
|
||||
template: '<div></div>',
|
||||
methods: { getOptionsForSave: sinon.stub() }
|
||||
}
|
||||
},
|
||||
plugins: [store, router]
|
||||
}
|
||||
})
|
||||
// click on the close icon of the first tab
|
||||
const firstTabCloseIcon = wrapper.findAll('.tab')[0].find('.close-icon')
|
||||
await firstTabCloseIcon.trigger('click')
|
||||
|
||||
// find 'Save and close' in the dialog
|
||||
const closeBtn = wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Save and close')
|
||||
|
||||
// click 'Save and close' in the dialog
|
||||
await closeBtn.trigger('click')
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// check that tab is closed
|
||||
expect(wrapper.findAllComponents({ name: 'Tab' })).to.have.lengthOf(1)
|
||||
// check that the open tab didn't change
|
||||
const firstTab = wrapper.findComponent({ name: 'Tab' })
|
||||
expect(firstTab.props('tab').name).to.equal('bar')
|
||||
expect(firstTab.props('tab').result).to.eql({
|
||||
columns: ['id'],
|
||||
values: {
|
||||
id: [1, 2, 3]
|
||||
}
|
||||
})
|
||||
expect(firstTab.props('tab')).to.eql(tab2)
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -413,7 +413,7 @@ describe('SQLite extensions', function () {
|
||||
WHERE ip.id <= p.id
|
||||
) AS path
|
||||
FROM tmp, json_each(filename_array) AS p
|
||||
WHERE p.id > 1 -- because the filenames start with the separator
|
||||
WHERE p.key > 0 -- because the filenames start with the separator
|
||||
`)
|
||||
expect(actual.values).to.eql({
|
||||
path: [
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('storedInquiries.js', () => {
|
||||
query: 'SELECT * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
createdAt: new Date(2021, 0, 1),
|
||||
createdAt: new Date(2021, 0, 1).toJSON(),
|
||||
isPredefined: true
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@ describe('storedInquiries.js', () => {
|
||||
expect(copy).to.have.property('query').which.equal(base.query)
|
||||
expect(copy).to.have.property('viewType').which.equal(base.viewType)
|
||||
expect(copy).to.have.property('viewOptions').which.eql(base.viewOptions)
|
||||
expect(copy).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||
expect(copy).to.have.property('createdAt')
|
||||
expect(new Date(copy.createdAt)).within(now, nowPlusMinute)
|
||||
expect(copy).to.not.have.property('isPredefined')
|
||||
})
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('tab.js', () => {
|
||||
query: undefined,
|
||||
viewOptions: undefined,
|
||||
isPredefined: undefined,
|
||||
updatedAt: undefined,
|
||||
viewType: 'chart',
|
||||
result: null,
|
||||
isGettingResults: false,
|
||||
@@ -42,7 +43,8 @@ describe('tab.js', () => {
|
||||
viewType: 'pivot',
|
||||
viewOptions: 'this is view options object',
|
||||
name: 'Foo inquiry',
|
||||
createdAt: '2022-12-05T18:30:30'
|
||||
createdAt: '2022-12-05T18:30:30',
|
||||
updatedAt: '2022-12-06T18:30:30'
|
||||
}
|
||||
|
||||
const newTab = new Tab(state, inquiry)
|
||||
@@ -53,6 +55,7 @@ describe('tab.js', () => {
|
||||
query: 'SELECT * from foo',
|
||||
viewOptions: 'this is view options object',
|
||||
isPredefined: undefined,
|
||||
updatedAt: '2022-12-06T18:30:30',
|
||||
viewType: 'pivot',
|
||||
result: null,
|
||||
isGettingResults: false,
|
||||
|
||||
@@ -19,7 +19,8 @@ describe('actions', () => {
|
||||
tempName: 'Untitled',
|
||||
viewType: 'chart',
|
||||
viewOptions: undefined,
|
||||
isSaved: false
|
||||
isSaved: false,
|
||||
updatedAt: undefined
|
||||
})
|
||||
expect(state.untitledLastIndex).to.equal(1)
|
||||
|
||||
@@ -30,7 +31,8 @@ describe('actions', () => {
|
||||
tempName: 'Untitled 1',
|
||||
viewType: 'chart',
|
||||
viewOptions: undefined,
|
||||
isSaved: false
|
||||
isSaved: false,
|
||||
updatedAt: undefined
|
||||
})
|
||||
expect(state.untitledLastIndex).to.equal(2)
|
||||
})
|
||||
@@ -40,16 +42,16 @@ describe('actions', () => {
|
||||
tabs: [],
|
||||
untitledLastIndex: 0
|
||||
}
|
||||
const tab = {
|
||||
const inquiry = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
query: 'SELECT * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: 'an object with view options',
|
||||
isSaved: true
|
||||
updatedAt: '2025-05-16T20:15:00Z'
|
||||
}
|
||||
await addTab({ state }, tab)
|
||||
expect(state.tabs[0]).to.include(tab)
|
||||
await addTab({ state }, inquiry)
|
||||
expect(state.tabs[0]).to.include(inquiry)
|
||||
expect(state.untitledLastIndex).to.equal(0)
|
||||
})
|
||||
|
||||
@@ -166,21 +168,26 @@ describe('actions', () => {
|
||||
newName: 'foo'
|
||||
}
|
||||
)
|
||||
expect(value.id).to.equal(tab.id)
|
||||
expect(value.id).not.to.equal(tab.id)
|
||||
expect(value.name).to.equal('foo')
|
||||
expect(value.query).to.equal(tab.query)
|
||||
expect(value.viewOptions).to.eql(['chart'])
|
||||
expect(value).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||
expect(value).to.have.property('createdAt')
|
||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
||||
expect(state.inquiries).to.eql([value])
|
||||
})
|
||||
|
||||
it('save updates existing inquiry in the storage', async () => {
|
||||
it('saveInquiry updates existing inquiry in the storage', async () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: null,
|
||||
name: 'foo',
|
||||
updatedAt: '2025-05-16T20:15:00Z',
|
||||
dataView: {
|
||||
getOptionsForSave() {
|
||||
return ['chart']
|
||||
@@ -189,34 +196,34 @@ describe('actions', () => {
|
||||
}
|
||||
|
||||
const state = {
|
||||
inquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: 'foo',
|
||||
createdAt: '2025-05-15T16:30:00Z',
|
||||
updatedAt: '2025-05-16T20:15:00Z'
|
||||
}
|
||||
],
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
const first = await saveInquiry(
|
||||
{ state },
|
||||
{
|
||||
inquiryTab: tab,
|
||||
newName: 'foo'
|
||||
}
|
||||
)
|
||||
|
||||
tab.name = 'foo'
|
||||
tab.query = 'select * from foo'
|
||||
await saveInquiry({ state }, { inquiryTab: tab })
|
||||
tab.query = 'select * from bar'
|
||||
await saveInquiry({ state }, { inquiryTab: tab, newName: '' })
|
||||
const inquiries = state.inquiries
|
||||
const second = inquiries[0]
|
||||
const updatedTab = inquiries[0]
|
||||
expect(inquiries).has.lengthOf(1)
|
||||
expect(second.id).to.equal(first.id)
|
||||
expect(second.name).to.equal(first.name)
|
||||
expect(second.query).to.equal(tab.query)
|
||||
expect(second.viewOptions).to.eql(['chart'])
|
||||
expect(new Date(second.createdAt).getTime()).to.equal(
|
||||
first.createdAt.getTime()
|
||||
)
|
||||
expect(updatedTab.id).to.equal(updatedTab.id)
|
||||
expect(updatedTab.name).to.equal(updatedTab.name)
|
||||
expect(updatedTab.query).to.equal(tab.query)
|
||||
expect(updatedTab.viewOptions).to.eql(['chart'])
|
||||
expect(updatedTab.createdAt).to.equal('2025-05-15T16:30:00Z')
|
||||
expect(new Date(updatedTab.updatedAt)).to.be.within(now, nowPlusMinute)
|
||||
})
|
||||
|
||||
it("save adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
||||
it("saveInquiry adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
const tab = {
|
||||
@@ -252,6 +259,95 @@ describe('actions', () => {
|
||||
expect(inquiries[0].name).to.equal('foo')
|
||||
expect(inquiries[0].query).to.equal(tab.query)
|
||||
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
||||
expect(new Date(inquiries[0].updatedAt)).to.be.within(now, nowPlusMinute)
|
||||
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
||||
})
|
||||
|
||||
it('saveInquiry adds new inquiry if newName is provided', async () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: 'foo',
|
||||
updatedAt: '2025-05-16T20:15:00Z',
|
||||
dataView: {
|
||||
getOptionsForSave() {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
}
|
||||
const inquiry = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: 'foo',
|
||||
createdAt: '2025-05-15T16:30:00Z',
|
||||
updatedAt: '2025-05-16T20:15:00Z'
|
||||
}
|
||||
const state = {
|
||||
inquiries: [inquiry],
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
const value = await saveInquiry(
|
||||
{ state },
|
||||
{
|
||||
inquiryTab: tab,
|
||||
newName: 'foo_new'
|
||||
}
|
||||
)
|
||||
expect(value.id).not.to.equal(tab.id)
|
||||
expect(value.name).to.equal('foo_new')
|
||||
expect(value.query).to.equal(tab.query)
|
||||
expect(value.viewOptions).to.eql(['chart'])
|
||||
expect(value).to.have.property('createdAt')
|
||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
||||
expect(state.inquiries).to.eql([inquiry, value])
|
||||
})
|
||||
|
||||
it('saveInquiry adds new inquiry if the inquiry is not in the storeage anymore', async () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: 'foo',
|
||||
updatedAt: '2025-05-16T20:15:00Z',
|
||||
dataView: {
|
||||
getOptionsForSave() {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
inquiries: [],
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
const value = await saveInquiry(
|
||||
{ state },
|
||||
{
|
||||
inquiryTab: tab,
|
||||
newName: ''
|
||||
}
|
||||
)
|
||||
expect(value.id).to.equal(tab.id)
|
||||
expect(value.name).to.equal('foo')
|
||||
expect(value.query).to.equal(tab.query)
|
||||
expect(value.viewOptions).to.eql(['chart'])
|
||||
expect(value).to.have.property('createdAt')
|
||||
expect(new Date(value.createdAt)).within(now, nowPlusMinute)
|
||||
expect(new Date(value.updatedAt)).within(now, nowPlusMinute)
|
||||
expect(state.inquiries).to.eql([value])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -34,7 +34,8 @@ describe('mutations', () => {
|
||||
viewType: 'chart',
|
||||
viewOptions: { here_are: 'chart settings' },
|
||||
isSaved: false,
|
||||
isPredefined: false
|
||||
isPredefined: false,
|
||||
updatedAt: '2025-05-15T15:30:00Z'
|
||||
}
|
||||
|
||||
const newValues = {
|
||||
@@ -43,6 +44,7 @@ describe('mutations', () => {
|
||||
query: 'SELECT * from bar',
|
||||
viewType: 'pivot',
|
||||
viewOptions: { here_are: 'pivot settings' },
|
||||
updatedAt: '2025-05-15T16:30:00Z',
|
||||
isSaved: true
|
||||
}
|
||||
|
||||
@@ -58,6 +60,7 @@ describe('mutations', () => {
|
||||
query: 'SELECT * from bar',
|
||||
viewType: 'pivot',
|
||||
viewOptions: { here_are: 'pivot settings' },
|
||||
updatedAt: '2025-05-15T16:30:00Z',
|
||||
isSaved: true
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,8 @@ import MainMenu from '@/views/MainView/MainMenu'
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
import { nextTick } from 'vue'
|
||||
import eventBus from '@/lib/eventBus'
|
||||
import actions from '@/store/actions'
|
||||
import mutations from '@/store/mutations'
|
||||
|
||||
let wrapper = null
|
||||
|
||||
@@ -26,7 +28,7 @@ describe('MainMenu.vue', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('Create and Save are visible only on /workspace page', async () => {
|
||||
it('Create, Save and Save as are visible only on /workspace page', async () => {
|
||||
const state = {
|
||||
currentTab: { query: '', execute: sinon.stub() },
|
||||
tabs: [{}],
|
||||
@@ -45,6 +47,8 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
||||
expect(wrapper.find('#save-btn').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#save-as-btn').exists()).to.equal(true)
|
||||
expect(wrapper.find('#save-as-btn').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||
wrapper.unmount()
|
||||
@@ -65,7 +69,7 @@ describe('MainMenu.vue', () => {
|
||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('Save is not visible if there is no tabs', () => {
|
||||
it('Save and Save as are not visible if there is no tabs', () => {
|
||||
const state = {
|
||||
currentTab: null,
|
||||
tabs: [],
|
||||
@@ -83,6 +87,8 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
expect(wrapper.find('#save-btn').exists()).to.equal(true)
|
||||
expect(wrapper.find('#save-btn').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#save-as-btn').exists()).to.equal(true)
|
||||
expect(wrapper.find('#save-as-btn').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#create-btn').exists()).to.equal(true)
|
||||
expect(wrapper.find('#create-btn').isVisible()).to.equal(true)
|
||||
})
|
||||
@@ -111,10 +117,12 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
const vm = wrapper.vm
|
||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#save-as-btn').element.disabled).to.equal(false)
|
||||
|
||||
store.state.tabs[0].isSaved = true
|
||||
await vm.$nextTick()
|
||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#save-as-btn').element.disabled).to.equal(false)
|
||||
})
|
||||
|
||||
it('Creates a tab', async () => {
|
||||
@@ -332,7 +340,7 @@ describe('MainMenu.vue', () => {
|
||||
expect(wrapper.vm.createNewInquiry.callCount).to.equal(4)
|
||||
})
|
||||
|
||||
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace', async () => {
|
||||
it('Ctrl S calls onSave if the tab is unsaved and route path is /workspace', async () => {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
@@ -353,36 +361,34 @@ describe('MainMenu.vue', () => {
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
sinon.stub(wrapper.vm, 'checkInquiryBeforeSave')
|
||||
sinon.stub(wrapper.vm, 'onSave')
|
||||
|
||||
const ctrlS = new KeyboardEvent('keydown', { key: 's', ctrlKey: true })
|
||||
const metaS = new KeyboardEvent('keydown', { key: 's', metaKey: true })
|
||||
// tab is unsaved and route is /workspace
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.checkInquiryBeforeSave.calledOnce).to.equal(true)
|
||||
expect(wrapper.vm.onSave.calledOnce).to.equal(true)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
||||
|
||||
// tab is saved and route is /workspace
|
||||
store.state.tabs[0].isSaved = true
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
||||
|
||||
// tab is unsaved and route is not /workspace
|
||||
wrapper.vm.$route.path = '/inquiries'
|
||||
store.state.tabs[0].isSaved = false
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.checkInquiryBeforeSave.calledTwice).to.equal(true)
|
||||
expect(wrapper.vm.onSave.calledTwice).to.equal(true)
|
||||
})
|
||||
|
||||
it('Saves the inquiry when no need the new name', async () => {
|
||||
it('Ctrl Shift S calls onSaveAs if route path is /workspace', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
@@ -392,6 +398,81 @@ describe('MainMenu.vue', () => {
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const store = createStore({ state })
|
||||
const $route = { path: '/workspace' }
|
||||
|
||||
wrapper = shallowMount(MainMenu, {
|
||||
global: {
|
||||
mocks: { $route },
|
||||
stubs: ['router-link'],
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
sinon.stub(wrapper.vm, 'onSaveAs')
|
||||
|
||||
const ctrlS = new KeyboardEvent('keydown', {
|
||||
key: 'S',
|
||||
ctrlKey: true,
|
||||
shiftKey: true
|
||||
})
|
||||
const metaS = new KeyboardEvent('keydown', {
|
||||
key: 'S',
|
||||
metaKey: true,
|
||||
shiftKey: true
|
||||
})
|
||||
// tab is unsaved and route is /workspace
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.onSaveAs.calledOnce).to.equal(true)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.onSaveAs.calledTwice).to.equal(true)
|
||||
|
||||
// tab is saved and route is /workspace
|
||||
store.state.tabs[0].isSaved = true
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.onSaveAs.calledThrice).to.equal(true)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
||||
|
||||
// tab is unsaved and route is not /workspace
|
||||
wrapper.vm.$route.path = '/inquiries'
|
||||
store.state.tabs[0].isSaved = false
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
||||
|
||||
// tab is saved and route is not /workspace
|
||||
wrapper.vm.$route.path = '/inquiries'
|
||||
store.state.tabs[0].isSaved = true
|
||||
document.dispatchEvent(ctrlS)
|
||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
||||
document.dispatchEvent(metaS)
|
||||
expect(wrapper.vm.onSaveAs.callCount).to.equal(4)
|
||||
})
|
||||
|
||||
it('Saves the inquiry when no need the new name and no update conflict', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: '2025-05-15T15:30:00Z',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: '2025-05-15T15:30:00Z',
|
||||
createdAt: '2025-05-14T15:30:00Z'
|
||||
}
|
||||
],
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
@@ -401,7 +482,8 @@ describe('MainMenu.vue', () => {
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-16T15:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
@@ -446,7 +528,8 @@ describe('MainMenu.vue', () => {
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true
|
||||
isSaved: true,
|
||||
updatedAt: '2025-05-16T15:30:00Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -456,6 +539,396 @@ describe('MainMenu.vue', () => {
|
||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
})
|
||||
|
||||
it('Inquiry conflict: overwrite', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: '2025-05-15T15:30:00Z',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM bar',
|
||||
updatedAt: '2025-05-15T16:30:00Z',
|
||||
createdAt: '2025-05-14T15:30:00Z'
|
||||
}
|
||||
],
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-16T17:30:00Z',
|
||||
createdAt: '2025-05-14T15:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
mocks: { $route },
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'app-diagnostic-info': true,
|
||||
teleport: true,
|
||||
transition: false
|
||||
},
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('#save-btn').trigger('click')
|
||||
|
||||
// check that the conflict dialog is open
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Inquiry saving conflict'
|
||||
)
|
||||
|
||||
// find Overwrite in the dialog and click
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Overwrite')
|
||||
.trigger('click')
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was saved via saveInquiry (newName='')
|
||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
||||
inquiryTab: state.currentTab,
|
||||
newName: ''
|
||||
})
|
||||
|
||||
// check that the tab was updated
|
||||
expect(
|
||||
mutations.updateTab.calledOnceWith(
|
||||
state,
|
||||
sinon.match({
|
||||
tab,
|
||||
newValues: {
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true,
|
||||
updatedAt: '2025-05-16T17:30:00Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
).to.equal(true)
|
||||
|
||||
// check that 'inquirySaved' event was triggered on eventBus
|
||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
})
|
||||
|
||||
it('Inquiry conflict after saving new inquiry: overwrite', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: null,
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: undefined,
|
||||
execute: sinon.stub(),
|
||||
dataView: { getOptionsForSave: sinon.stub() },
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
inquiries: [],
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
mocks: { $route },
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'app-diagnostic-info': true,
|
||||
teleport: true,
|
||||
transition: false
|
||||
},
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('#save-btn').trigger('click')
|
||||
|
||||
// check that Save dialog is open
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Save inquiry'
|
||||
)
|
||||
|
||||
// enter the name
|
||||
await wrapper.find('.dialog-body input').setValue('foo')
|
||||
|
||||
// find Save in the dialog and click
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
|
||||
// check that now there is one inquiry saved
|
||||
expect(state.inquiries.length).to.equal(1)
|
||||
expect(state.inquiries[0].name).to.equal('foo')
|
||||
expect(state.tabs[0].name).to.equal('foo')
|
||||
|
||||
// change the inquiry in store (like it's updated in another tab)
|
||||
store.state.inquiries[0].query = 'SELECT * FROM foo_updated_in_another_tab'
|
||||
store.state.inquiries[0].updatedAt = '2025-05-15T00:00:10Z'
|
||||
store.state.currentTab.query = 'SELECT * FROM foo_new'
|
||||
store.state.currentTab.isSaved = false
|
||||
await nextTick()
|
||||
await wrapper.find('#save-btn').trigger('click')
|
||||
|
||||
// check that the conflict dialog is open
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Inquiry saving conflict'
|
||||
)
|
||||
|
||||
// find Overwrite in the dialog and click
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Overwrite')
|
||||
.trigger('click')
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
|
||||
// check that it's still one inquiry saved
|
||||
expect(state.inquiries.length).to.equal(1)
|
||||
expect(state.inquiries[0].name).to.equal('foo')
|
||||
expect(state.tabs[0].name).to.equal('foo')
|
||||
expect(state.inquiries[0].query).to.equal('SELECT * FROM foo_new')
|
||||
expect(state.tabs[0].query).to.equal('SELECT * FROM foo_new')
|
||||
})
|
||||
|
||||
it('Inquiry conflict: save as new', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: '2025-05-15T15:30:00Z',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM bar',
|
||||
updatedAt: '2025-05-15T16:30:00Z',
|
||||
createdAt: '2025-05-14T15:30:00Z'
|
||||
}
|
||||
],
|
||||
tabs: [tab]
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo_new',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-16T17:30:00Z',
|
||||
createdAt: '2025-05-16T17:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
mocks: { $route },
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'app-diagnostic-info': true,
|
||||
teleport: true,
|
||||
transition: false
|
||||
},
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('#save-btn').trigger('click')
|
||||
// check that the conflict dialog is open
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Inquiry saving conflict'
|
||||
)
|
||||
|
||||
await clock.tick(100)
|
||||
|
||||
// find "Save as new" in the dialog and click
|
||||
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Save as new')
|
||||
.trigger('click')
|
||||
|
||||
// Hiding any dialog is done with tiny animation. Give time to finish it:
|
||||
await clock.tick(100)
|
||||
// Note: don't call nextTick before clock.tick. That leads to extra trap in
|
||||
// trapStack and the test fails with focus-trap error in afterEach hook
|
||||
// when unmount the component
|
||||
|
||||
// check that only one dialog open
|
||||
expect(wrapper.findAll('.dialog.vfm').length).to.equal(1)
|
||||
// enter the new name
|
||||
await wrapper.find('.dialog-body input').setValue('foo_new')
|
||||
|
||||
// find Save in the dialog and click
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was saved via saveInquiry (newName='foo_new')
|
||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
||||
inquiryTab: state.currentTab,
|
||||
newName: 'foo_new'
|
||||
})
|
||||
|
||||
// check that the tab was updated
|
||||
expect(
|
||||
mutations.updateTab.calledOnceWith(
|
||||
state,
|
||||
sinon.match({
|
||||
tab,
|
||||
newValues: {
|
||||
name: 'foo_new',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true,
|
||||
updatedAt: '2025-05-16T17:30:00Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
).to.equal(true)
|
||||
|
||||
// check that 'inquirySaved' event was triggered on eventBus
|
||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
})
|
||||
|
||||
it('Inquiry conflict: cancel', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: '2025-05-15T15:30:00Z',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM bar',
|
||||
updatedAt: '2025-05-15T16:30:00Z',
|
||||
createdAt: '2025-05-14T15:30:00Z'
|
||||
}
|
||||
],
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub()
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
mocks: { $route },
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'app-diagnostic-info': true,
|
||||
teleport: true,
|
||||
transition: false
|
||||
},
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('#save-btn').trigger('click')
|
||||
|
||||
// check that the conflict dialog is open
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Inquiry saving conflict'
|
||||
)
|
||||
|
||||
// find Cancel in the dialog and click
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Cancel')
|
||||
.trigger('click')
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was not saved via storedInquiries.save
|
||||
expect(actions.saveInquiry.called).to.equal(false)
|
||||
|
||||
// check that the tab was not updated
|
||||
expect(mutations.updateTab.called).to.equal(false)
|
||||
|
||||
// check that 'inquirySaved' event is not listened on eventBus
|
||||
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
})
|
||||
|
||||
it('Shows en error when the new name is needed but not specifyied', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
@@ -463,7 +936,8 @@ describe('MainMenu.vue', () => {
|
||||
tempName: 'Untitled',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
isSaved: false,
|
||||
updatedAt: '2025-05-15T15:30:00Z'
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
@@ -479,7 +953,8 @@ describe('MainMenu.vue', () => {
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-16T15:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
@@ -522,14 +997,15 @@ describe('MainMenu.vue', () => {
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
})
|
||||
|
||||
it('Saves the inquiry with a new name', async () => {
|
||||
it('Saves the new inquiry with a new name', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
isSaved: false
|
||||
isSaved: false,
|
||||
updatedAt: undefined
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
@@ -542,10 +1018,11 @@ describe('MainMenu.vue', () => {
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-15T15:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
@@ -583,8 +1060,6 @@ describe('MainMenu.vue', () => {
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
await nextTick()
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
@@ -604,11 +1079,12 @@ describe('MainMenu.vue', () => {
|
||||
tab,
|
||||
newValues: {
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true
|
||||
isSaved: true,
|
||||
updatedAt: '2025-05-15T15:30:00Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -650,7 +1126,8 @@ describe('MainMenu.vue', () => {
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-15T15:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
@@ -691,8 +1168,6 @@ describe('MainMenu.vue', () => {
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
await nextTick()
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
@@ -716,7 +1191,8 @@ describe('MainMenu.vue', () => {
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true
|
||||
isSaved: true,
|
||||
updatedAt: '2025-05-15T15:30:00Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -724,19 +1200,6 @@ describe('MainMenu.vue', () => {
|
||||
|
||||
// check that 'inquirySaved' event was triggered on eventBus
|
||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
|
||||
// We saved predefined inquiry, so the tab will be created again
|
||||
// (because of new id) and it will be without sql result and has default view - table.
|
||||
// That's why we need to restore data and view.
|
||||
// Check that result and view are preserved in the currentTab:
|
||||
expect(state.currentTab.viewType).to.equal('chart')
|
||||
expect(state.currentTab.result).to.eql({
|
||||
columns: ['id', 'name'],
|
||||
values: [
|
||||
[1, 'Harry Potter'],
|
||||
[2, 'Drako Malfoy']
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('Cancel saving', async () => {
|
||||
@@ -761,7 +1224,7 @@ describe('MainMenu.vue', () => {
|
||||
name: 'bar',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
chart: []
|
||||
viewType: 'chart'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
@@ -809,4 +1272,110 @@ describe('MainMenu.vue', () => {
|
||||
// check that 'inquirySaved' event is not listened on eventBus
|
||||
expect(eventBus.$off.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
})
|
||||
|
||||
it('Save the inquiry as new', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
updatedAt: '2025-05-15T15:30:00Z',
|
||||
execute: sinon.stub(),
|
||||
isSaved: true
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM bar',
|
||||
updatedAt: '2025-05-15T16:30:00Z',
|
||||
createdAt: '2025-05-14T15:30:00Z'
|
||||
}
|
||||
],
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo_new',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
updatedAt: '2025-05-16T17:30:00Z',
|
||||
createdAt: '2025-05-16T17:30:00Z'
|
||||
})
|
||||
}
|
||||
const store = createStore({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
mocks: { $route },
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
'app-diagnostic-info': true,
|
||||
teleport: true,
|
||||
transition: false
|
||||
},
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('#save-as-btn').trigger('click')
|
||||
|
||||
// check that Save dialog is open
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Save inquiry'
|
||||
)
|
||||
|
||||
// enter the new name
|
||||
await wrapper.find('.dialog-body input').setValue('foo_new')
|
||||
|
||||
// find Save in the dialog and click
|
||||
await wrapper
|
||||
.findAll('.dialog-buttons-container button')
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
// check that the dialog is closed
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was saved via saveInquiry (newName='foo_new')
|
||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
||||
inquiryTab: state.currentTab,
|
||||
newName: 'foo_new'
|
||||
})
|
||||
|
||||
// check that the tab was updated
|
||||
expect(
|
||||
mutations.updateTab.calledOnceWith(
|
||||
state,
|
||||
sinon.match({
|
||||
tab,
|
||||
newValues: {
|
||||
name: 'foo_new',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true,
|
||||
updatedAt: '2025-05-16T17:30:00Z'
|
||||
}
|
||||
})
|
||||
)
|
||||
).to.equal(true)
|
||||
|
||||
// check that 'inquirySaved' event was triggered on eventBus
|
||||
expect(eventBus.$emit.calledOnceWith('inquirySaved')).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,10 +123,8 @@ describe('Chart.vue', () => {
|
||||
const newContainerWidth = initialContainerWidth * 2 || 1000
|
||||
const newContainerHeight = initialContainerHeight * 2 || 2000
|
||||
|
||||
wrapper.find('.chart-container').wrapperElement.parentElement.style.width =
|
||||
`${newContainerWidth}px`
|
||||
wrapper.find('.chart-container').wrapperElement.parentElement.style.height =
|
||||
`${newContainerHeight}px`
|
||||
container.style.width = `${newContainerWidth}px`
|
||||
container.style.height = `${newContainerHeight}px`
|
||||
|
||||
await flushPromises()
|
||||
|
||||
@@ -179,4 +177,34 @@ describe('Chart.vue', () => {
|
||||
expect(fIo.downloadFromUrl.calledOnceWith(url, 'chart'))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('dataSources are passed correctly', async () => {
|
||||
const dataSources = {
|
||||
name: ['Gryffindor'],
|
||||
points: [80]
|
||||
}
|
||||
|
||||
const wrapper = mount(Chart, {
|
||||
attachTo: document.body,
|
||||
props: {
|
||||
dataSources
|
||||
},
|
||||
global: {
|
||||
mocks: { $store }
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
await wrapper.find('button.js-add-button').wrapperElement.click()
|
||||
|
||||
await flushPromises()
|
||||
|
||||
await wrapper
|
||||
.find('.field .dropdown-container .Select__indicator')
|
||||
.wrapperElement.dispatchEvent(
|
||||
new MouseEvent('mousedown', { bubbles: true })
|
||||
)
|
||||
|
||||
expect(wrapper.find('.Select__menu').text()).to.contain('name' + 'points')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils'
|
||||
import DataView from '@/views/MainView/Workspace/Tabs/Tab/DataView'
|
||||
import sinon from 'sinon'
|
||||
import { nextTick } from 'vue'
|
||||
import cIo from '@/lib/utils/clipboardIo'
|
||||
|
||||
describe('DataView.vue', () => {
|
||||
const $store = { state: { isWorkspaceVisible: true } }
|
||||
@@ -64,7 +65,7 @@ describe('DataView.vue', () => {
|
||||
|
||||
// Find chart and spy the method
|
||||
const chart = wrapper.findComponent({ name: 'Chart' }).vm
|
||||
sinon.spy(chart, 'saveAsSvg')
|
||||
sinon.stub(chart, 'saveAsSvg')
|
||||
|
||||
// Export to svg
|
||||
const svgBtn = wrapper.findComponent({ ref: 'svgExportBtn' })
|
||||
@@ -77,7 +78,7 @@ describe('DataView.vue', () => {
|
||||
|
||||
// Find pivot and spy the method
|
||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||
sinon.spy(pivot, 'saveAsSvg')
|
||||
sinon.stub(pivot, 'saveAsSvg')
|
||||
|
||||
// Switch to Custom Chart renderer
|
||||
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||
@@ -146,6 +147,7 @@ describe('DataView.vue', () => {
|
||||
|
||||
it('copy to clipboard more than 1 sec', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
||||
sinon.stub(cIo, 'copyImage')
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(DataView, {
|
||||
attachTo: document.body,
|
||||
@@ -165,7 +167,7 @@ describe('DataView.vue', () => {
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Copy to clipboard'
|
||||
)
|
||||
@@ -180,11 +182,10 @@ describe('DataView.vue', () => {
|
||||
// Wait untill prepareCopy is finished
|
||||
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
||||
|
||||
// ... with Ready message...
|
||||
expect(wrapper.find('.dialog-body').text()).to.equal('Image is ready')
|
||||
@@ -196,12 +197,13 @@ describe('DataView.vue', () => {
|
||||
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('copy to clipboard less than 1 sec', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'write').resolves()
|
||||
sinon.stub(cIo, 'copyImage')
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(DataView, {
|
||||
attachTo: document.body,
|
||||
@@ -226,10 +228,9 @@ describe('DataView.vue', () => {
|
||||
// Wait untill prepareCopy is finished
|
||||
await wrapper.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||
|
||||
await nextTick()
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
// copyToClipboard is called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||
wrapper.unmount()
|
||||
@@ -270,7 +271,7 @@ describe('DataView.vue', () => {
|
||||
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
// copyToClipboard is not called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||
wrapper.unmount()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import Pivot from '@/views/MainView/Workspace/Tabs/Tab/DataView/Pivot'
|
||||
import chartHelper from '@/lib/chartHelper'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
@@ -533,4 +533,57 @@ describe('Pivot.vue', () => {
|
||||
fIo.downloadFromUrl.calledOnceWith('canvas data url', 'pivot')
|
||||
).to.equal(true)
|
||||
})
|
||||
|
||||
it('resizes standart chart', async () => {
|
||||
const wrapper = mount(Pivot, {
|
||||
global: {
|
||||
mocks: { $store: { state: { isWorkspaceVisible: true } } }
|
||||
},
|
||||
props: {
|
||||
dataSources: {
|
||||
item: ['foo', 'bar', 'bar', 'bar'],
|
||||
year: [2021, 2021, 2020, 2020]
|
||||
},
|
||||
initOptions: {
|
||||
rows: ['item'],
|
||||
cols: ['year'],
|
||||
colOrder: 'key_a_to_z',
|
||||
rowOrder: 'key_a_to_z',
|
||||
aggregatorName: 'Count',
|
||||
vals: [],
|
||||
renderer: $.pivotUtilities.renderers['Bar Chart'],
|
||||
rendererName: 'Bar Chart'
|
||||
}
|
||||
},
|
||||
attachTo: container
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const plotContainer = wrapper.find('.pivot-output').wrapperElement
|
||||
const plot = wrapper.find('.svg-container').wrapperElement
|
||||
|
||||
const initialContainerWidth = plotContainer.scrollWidth
|
||||
const initialContainerHeight = plotContainer.scrollHeight
|
||||
|
||||
const initialPlotWidth = plot.scrollWidth
|
||||
const initialPlotHeight = plot.scrollHeight
|
||||
|
||||
const newContainerWidth = initialContainerWidth * 2 || 1000
|
||||
const newContainerHeight = initialContainerHeight * 2 || 2000
|
||||
|
||||
plotContainer.style.width = `${newContainerWidth}px`
|
||||
plotContainer.style.height = `${newContainerHeight}px`
|
||||
|
||||
await flushPromises()
|
||||
|
||||
const plotAfterResize = wrapper.find('.svg-container').wrapperElement
|
||||
expect(plotAfterResize.scrollWidth).not.to.equal(initialPlotWidth)
|
||||
|
||||
expect(plotAfterResize.scrollWidth.scrollHeight).not.to.equal(
|
||||
initialPlotHeight
|
||||
)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('RunResult.vue', () => {
|
||||
await nextTick()
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Copy to clipboard'
|
||||
)
|
||||
@@ -91,7 +91,7 @@ describe('RunResult.vue', () => {
|
||||
await nextTick()
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
||||
|
||||
// ... with Ready message...
|
||||
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
||||
@@ -104,7 +104,7 @@ describe('RunResult.vue', () => {
|
||||
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
@@ -139,11 +139,10 @@ describe('RunResult.vue', () => {
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
await clock.tick(0)
|
||||
await nextTick()
|
||||
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
// copyToClipboard is called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||
wrapper.unmount()
|
||||
@@ -188,7 +187,7 @@ describe('RunResult.vue', () => {
|
||||
.trigger('click')
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm').exists()).to.equal(false)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
// copyToClipboard is not called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||
wrapper.unmount()
|
||||
|
||||
@@ -5,6 +5,9 @@ import mutations from '@/store/mutations'
|
||||
import { createStore } from 'vuex'
|
||||
import Tabs from '@/views/MainView/Workspace/Tabs'
|
||||
import eventBus from '@/lib/eventBus'
|
||||
import { nextTick } from 'vue'
|
||||
import cIo from '@/lib/utils/clipboardIo'
|
||||
import csv from '@/lib/csv'
|
||||
|
||||
describe('Tabs.vue', () => {
|
||||
let clock
|
||||
@@ -46,7 +49,7 @@ describe('Tabs.vue', () => {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: true
|
||||
},
|
||||
{
|
||||
@@ -54,7 +57,7 @@ describe('Tabs.vue', () => {
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: '',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: false
|
||||
}
|
||||
],
|
||||
@@ -97,7 +100,7 @@ describe('Tabs.vue', () => {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: true
|
||||
},
|
||||
{
|
||||
@@ -105,7 +108,7 @@ describe('Tabs.vue', () => {
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: '',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: false
|
||||
}
|
||||
],
|
||||
@@ -436,7 +439,7 @@ describe('Tabs.vue', () => {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: true
|
||||
},
|
||||
{
|
||||
@@ -444,7 +447,7 @@ describe('Tabs.vue', () => {
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: '',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: false
|
||||
}
|
||||
],
|
||||
@@ -477,7 +480,7 @@ describe('Tabs.vue', () => {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
chart: [],
|
||||
viewType: 'chart',
|
||||
isSaved: true
|
||||
}
|
||||
],
|
||||
@@ -501,4 +504,216 @@ describe('Tabs.vue', () => {
|
||||
expect(event.preventDefault.calledOnce).to.equal(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('Copy image to clipboard dialog works in the context of the tab', async () => {
|
||||
// mock store state - 2 inquiries open
|
||||
const state = {
|
||||
tabs: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: undefined,
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'hidden',
|
||||
dataView: 'bottom'
|
||||
},
|
||||
isSaved: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: '',
|
||||
viewType: 'chart',
|
||||
viewOptions: undefined,
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'hidden',
|
||||
dataView: 'bottom'
|
||||
},
|
||||
isSaved: false
|
||||
}
|
||||
],
|
||||
currentTabId: 2
|
||||
}
|
||||
const store = createStore({ state })
|
||||
sinon.stub(cIo, 'copyImage')
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tabs, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
stubs: { teleport: true, transition: true, RouterLink: true },
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
const firstTabDataView = wrapper
|
||||
.findAllComponents({ name: 'Tab' })[0]
|
||||
.findComponent({ name: 'DataView' })
|
||||
|
||||
const secondTabDataView = wrapper
|
||||
.findAllComponents({ name: 'Tab' })[1]
|
||||
.findComponent({ name: 'DataView' })
|
||||
|
||||
// Stub prepareCopy method so it takes long and copy dialog will be shown
|
||||
sinon
|
||||
.stub(firstTabDataView.vm.$refs.viewComponent, 'prepareCopy')
|
||||
.callsFake(async () => {
|
||||
await clock.tick(5000)
|
||||
return 'prepareCopy result in tab 1'
|
||||
})
|
||||
|
||||
sinon
|
||||
.stub(secondTabDataView.vm.$refs.viewComponent, 'prepareCopy')
|
||||
.callsFake(async () => {
|
||||
await clock.tick(5000)
|
||||
return 'prepareCopy result in tab 2'
|
||||
})
|
||||
|
||||
// Click Copy to clipboard button in the second tab
|
||||
const copyBtn = secondTabDataView.findComponent({
|
||||
ref: 'copyToClipboardBtn'
|
||||
})
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Copy to clipboard'
|
||||
)
|
||||
|
||||
// Switch to microtasks (let prepareCopy run)
|
||||
await clock.tick(0)
|
||||
// Wait untill prepareCopy is finished
|
||||
await secondTabDataView.vm.$refs.viewComponent.prepareCopy.returnValues[0]
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Click copy button in the dialog
|
||||
await wrapper
|
||||
.find('.dialog-buttons-container button.primary')
|
||||
.trigger('click')
|
||||
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
|
||||
// copyImage is called with prepare copy result calculated in tab 2, not null
|
||||
// i.e. the dialog works in the tab 2 context
|
||||
expect(
|
||||
cIo.copyImage.calledOnceWith('prepareCopy result in tab 2')
|
||||
).to.equal(true)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('Copy CSV to clipboard dialog works in the context of the tab', async () => {
|
||||
// mock store state - 2 inquiries open
|
||||
const state = {
|
||||
tabs: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: undefined,
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'bottom',
|
||||
dataView: 'hidden'
|
||||
},
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1, 2, 3],
|
||||
name: ['Gryffindor', 'Hufflepuff']
|
||||
}
|
||||
},
|
||||
isSaved: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: '',
|
||||
viewType: 'chart',
|
||||
viewOptions: undefined,
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'bottom',
|
||||
dataView: 'hidden'
|
||||
},
|
||||
result: {
|
||||
columns: ['name', 'points'],
|
||||
values: {
|
||||
name: ['Gryffindor', 'Hufflepuff', 'Ravenclaw', 'Slytherin'],
|
||||
points: [100, 90, 95, 80]
|
||||
}
|
||||
},
|
||||
isSaved: false
|
||||
}
|
||||
],
|
||||
currentTabId: 2
|
||||
}
|
||||
const store = createStore({ state })
|
||||
sinon.stub(cIo, 'copyText')
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(Tabs, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
stubs: { teleport: true, transition: true, RouterLink: true },
|
||||
plugins: [store]
|
||||
}
|
||||
})
|
||||
|
||||
const secondTabRunResult = wrapper
|
||||
.findAllComponents({ name: 'Tab' })[1]
|
||||
.findComponent({ name: 'RunResult' })
|
||||
|
||||
// Stub prepareCopy method so it takes long and copy dialog will be shown
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(5000)
|
||||
return 'csv serialize result'
|
||||
})
|
||||
|
||||
// Click Copy to clipboard button in the second tab
|
||||
const copyBtn = secondTabRunResult.findComponent({
|
||||
ref: 'copyToClipboardBtn'
|
||||
})
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog.vfm .dialog-header').text()).to.contain(
|
||||
'Copy to clipboard'
|
||||
)
|
||||
|
||||
// Switch to microtasks (let prepareCopy run)
|
||||
await clock.tick(0)
|
||||
await nextTick()
|
||||
|
||||
// Click copy button in the dialog
|
||||
await wrapper
|
||||
.find('.dialog-buttons-container button.primary')
|
||||
.trigger('click')
|
||||
|
||||
// The dialog is not shown...
|
||||
await clock.tick(100)
|
||||
expect(wrapper.find('.dialog.vfm .vfm__content').exists()).to.equal(false)
|
||||
|
||||
// copyText is called with 'csv serialize result' calculated in tab 2, not null
|
||||
// i.e. the dialog works in the tab 2 context
|
||||
expect(
|
||||
cIo.copyText.calledOnceWith(
|
||||
'csv serialize result',
|
||||
'CSV copied to clipboard successfully'
|
||||
)
|
||||
).to.equal(true)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user