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

feat: ability to edit table fields

This commit is contained in:
2020-11-13 12:39:40 +01:00
parent ae47a978c1
commit 249926b8e0
20 changed files with 1085 additions and 194 deletions

View File

@ -48,7 +48,7 @@ export default {
props: {
size: {
type: String,
validator: prop => ['small', 'medium', 'large'].includes(prop),
validator: prop => ['small', 'medium', '400', 'large'].includes(prop),
default: 'small'
},
confirmText: String,
@ -67,6 +67,8 @@ export default {
modalSizeClass () {
if (this.size === 'small')
return 'modal-sm';
if (this.size === '400')
return 'modal-400';
else if (this.size === 'large')
return 'modal-lg';
else return '';
@ -86,7 +88,12 @@ export default {
</script>
<style scoped>
.modal.modal-sm .modal-container {
padding: 0;
}
.modal-400 .modal-container {
max-width: 400px;
}
.modal.modal-sm .modal-container {
padding: 0;
}
</style>

View File

@ -3,13 +3,20 @@
<div class="workspace-query-runner column col-12">
<div class="workspace-query-runner-footer">
<div class="workspace-query-buttons">
<button class="btn btn-primary btn-sm">
<button
class="btn btn-primary btn-sm"
:disabled="!isChanged"
:class="{'loading':isSaving}"
@click="saveChanges"
>
<span>{{ $t('word.save') }}</span>
<i class="mdi mdi-24px mdi-content-save ml-1" />
</button>
<button
:disabled="!isChanged"
class="btn btn-link btn-sm mr-0"
:title="$t('message.clearChanges')"
@click="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
@ -40,7 +47,7 @@
<WorkspacePropsTable
v-if="localFields"
ref="queryTable"
:results="localFields"
:fields="localFields"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:table="table"
@ -52,9 +59,10 @@
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'WorkspacePropsTab',
@ -69,7 +77,10 @@ export default {
return {
tabUid: 'prop',
isQuering: false,
isSaving: false,
originalFields: [],
localFields: [],
originalKeyUsage: [],
localKeyUsage: [],
lastTable: null,
isAddModal: false
@ -87,6 +98,9 @@ export default {
},
schema () {
return this.workspace.breadcrumbs.schema;
},
isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) || JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage);
}
},
watch: {
@ -103,25 +117,13 @@ export default {
}
}
},
created () {
this.getFieldsData();
window.addEventListener('keydown', this.onKey);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification',
setTabFields: 'workspaces/setTabFields',
setTabKeyUsage: 'workspaces/setTabKeyUsage'
addNotification: 'notifications/addNotification'
}),
async getFieldsData () {
if (!this.table) return;
this.isQuering = true;
const fieldsArr = [];
const keysArr = [];
// if table changes clear cached values
if (this.lastTable !== this.table)
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
const params = {
uid: this.connection.uid,
@ -132,8 +134,10 @@ export default {
try { // Columns data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
this.localFields = response;
fieldsArr.push(response);
this.originalFields = response.map(field => {
return { ...field, _id: uidGen() };
});
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
}
else
this.addNotification({ status: 'error', message: response });
@ -146,8 +150,8 @@ export default {
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') {
this.localKeyUsage = response;
keysArr.push(response);
this.originalKeyUsage = response;
this.localKeyUsage = JSON.parse(JSON.stringify(response));
}
else
this.addNotification({ status: 'error', message: response });
@ -156,35 +160,62 @@ export default {
this.addNotification({ status: 'error', message: err.stack });
}
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: fieldsArr });
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: keysArr });
this.isQuering = false;
},
reloadFields () {
this.getFieldsData();
async saveChanges () {
if (this.isSaving) return;
this.isSaving = true;
const originalIDs = this.originalFields.reduce((acc, curr) => [...acc, curr._id], []);
const localIDs = this.localFields.reduce((acc, curr) => [...acc, curr._id], []);
const additions = this.localFields.filter(field => !originalIDs.includes(field._id));
const deletions = this.originalFields.filter(field => !localIDs.includes(field._id));
// Changes
const changes = [];
this.originalFields.forEach((originalField, oI) => {
const lI = this.localFields.findIndex(localField => localField._id === originalField._id);
const originalSibling = oI > 0 ? this.originalFields[oI - 1]._id : false;
const localSibling = lI > 0 ? this.localFields[lI - 1]._id : false;
const after = lI > 0 ? this.localFields[lI - 1].name : false;
const orgName = originalField.name;
if (JSON.stringify(originalField) !== JSON.stringify(this.localFields[lI]) || originalSibling !== localSibling)
changes.push({ ...this.localFields[lI], after, orgName });
});
const params = {
uid: this.connection.uid,
schema: this.schema,
table: this.workspace.breadcrumbs.table,
additions,
changes,
deletions
};
try { // Key usage (foreign keys)
const { status, response } = await Tables.alterTable(params);
if (status === 'success')
this.getFieldsData();
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
this.isSaving = false;
},
clearChanges () {
this.localFields = JSON.parse(JSON.stringify(this.originalFields));
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
},
showAddModal () {
this.isAddModal = true;
},
hideAddModal () {
this.isAddModal = false;
},
onKey (e) {
e.stopPropagation();
if (e.key === 'F5')
this.reloadFields();
},
setRefreshInterval () {
if (this.refreshInterval)
clearInterval(this.refreshInterval);
if (+this.autorefreshTimer) {
this.refreshInterval = setInterval(() => {
if (!this.isQuering)
this.reloadFields();
}, this.autorefreshTimer * 1000);
}
}
}
};

View File

@ -89,79 +89,36 @@
</div>
</div>
</div>
<div ref="resultTable" class="tbody">
<div
v-for="row in results"
:key="row.name"
class="tr"
>
<div class="td">
<div class="row-draggable">
<i class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ row.order }}
</div>
</div>
<div class="td">
<i
v-if="row.key"
:title="keyName(row.key)"
class="mdi mdi-key column-key c-help pl-1"
:class="`key-${row.key}`"
/>
</div>
<div class="td">
{{ row.name }}
</div>
<div class="td text-uppercase" :class="`type-${row.type}`">
{{ row.type }}
</div>
<div class="td type-int">
{{ row.numLength || row.charLength || row.datePrecision }}
</div>
<div class="td">
<label class="form-checkbox">
<input type="checkbox" :checked="row.unsigned ">
<i class="form-icon" />
</label>
</div>
<div class="td">
<label class="form-checkbox">
<input type="checkbox" :checked="row.nullable ">
<i class="form-icon" />
</label>
</div>
<div class="td">
<label class="form-checkbox">
<input type="checkbox" :checked="row.zerofill ">
<i class="form-icon" />
</label>
</div>
<div class="td">
{{ (row.autoIncrement ? 'AUTO_INCREMENT' : false) || row.default }}
</div>
<div class="td type-varchar">
{{ row.comment }}
</div>
<div class="td">
{{ row.collation }}
</div>
</div>
</div>
<draggable
ref="resultTable"
:list="fields"
class="tbody"
handle=".row-draggable"
>
<TableRow
v-for="row in fields"
:key="row._id"
:row="row"
:data-types="dataTypes"
/>
</draggable>
</div>
</div>
</template>
<script>
import TableContext from '@/components/WorkspaceQueryTableContext';
import { mapActions, mapGetters } from 'vuex';
import draggable from 'vuedraggable';
import TableRow from '@/components/WorkspacePropsTableRow';
export default {
name: 'WorkspacePropsTable',
components: {
TableContext
TableRow,
draggable
},
props: {
results: Array,
fields: Array,
tabUid: [String, Number],
connUid: String,
table: String,
@ -187,13 +144,15 @@ export default {
workspaceSchema () {
return this.getWorkspace(this.connUid).breadcrumbs.schema;
},
dataTypes () {
return this.getWorkspace(this.connUid).dataTypes;
},
primaryField () {
return this.results.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
},
tabProperties () {
return this.getWorkspaceTab(this.tabUid);
}
},
updated () {
if (this.$refs.propTable)
@ -212,18 +171,6 @@ export default {
...mapActions({
addNotification: 'notifications/addNotification'
}),
keyName (key) {
switch (key) {
case 'pri':
return 'PRIMARY';
case 'uni':
return 'UNIQUE';
case 'mul':
return 'INDEX';
default:
return 'UNKNOWN ' + key;
}
},
resizeResults () {
if (this.$refs.resultTable) {
const el = this.$refs.tableWrapper;
@ -258,34 +205,4 @@ export default {
overflow: hidden;
}
}
.row-draggable {
position: relative;
text-align: right;
padding-left: 28px;
cursor: grab;
.row-draggable-icon {
position: absolute;
left: 0;
font-size: 22px;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.form-checkbox {
padding: 0;
margin: 0;
line-height: 1;
min-height: auto;
.form-icon {
top: 0.15rem;
left: calc(50% - 8px);
}
}
</style>

View File

@ -0,0 +1,516 @@
<template>
<div class="tr">
<div class="td">
<div class="row-draggable">
<i class="mdi mdi-drag-horizontal row-draggable-icon" />
{{ localRow.order }}
</div>
</div>
<div class="td" tabindex="0">
<i
v-if="localRow.key"
:title="keyName(localRow.key)"
class="mdi mdi-key column-key c-help pl-1"
:class="`key-${localRow.key}`"
/>
</div>
<div class="td">
<span
v-if="!isInlineEditor.name"
class="cell-content"
@dblclick="editON($event, localRow.name , 'name')"
>
{{ localRow.name }}
</span>
<input
v-else
ref="editField"
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
</div>
<div class="td text-uppercase text-left" :class="`type-${lowerCase(localRow.type)}`">
<span
v-if="!isInlineEditor.type"
class="cell-content"
@dblclick="editON($event, localRow.type.toUpperCase(), 'type')"
>
{{ localRow.type }}
</span>
<select
v-else
ref="editField"
v-model="editingContent"
class="editable-field px-1 text-uppercase"
@blur="editOFF"
>
<optgroup
v-for="group in dataTypes"
:key="group.group"
:label="group.group"
>
<option
v-for="type in group.types"
:key="type.name"
:selected="localRow.type.toUpperCase() === type.name"
:value="type.name"
>
{{ type.name }}
</option>
</optgroup>
</select>
</div>
<div class="td type-int">
<template v-if="fieldType.length">
<span
v-if="!isInlineEditor.length"
class="cell-content"
@dblclick="editON($event, localLength, 'length')"
>
{{ localLength }}
</span>
<input
v-else
ref="editField"
v-model="editingContent"
type="number"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
</template>
</div>
<div class="td">
<label class="form-checkbox">
<input
v-model="localRow.unsigned"
type="checkbox"
:disabled="!fieldType.unsigned"
>
<i class="form-icon" />
</label>
</div>
<div class="td">
<label class="form-checkbox">
<input
v-model="localRow.nullable"
type="checkbox"
:disabled="localRow.key === 'pri'"
>
<i class="form-icon" />
</label>
</div>
<div class="td">
<label class="form-checkbox">
<input
v-model="localRow.zerofill"
type="checkbox"
:disabled="!fieldType.zerofill"
>
<i class="form-icon" />
</label>
</div>
<div class="td">
<span class="cell-content" @dblclick="editON($event, localRow.default, 'default')">
{{ fieldDefault }}
</span>
</div>
<div class="td type-varchar">
<span
v-if="!isInlineEditor.comment"
class="cell-content"
@dblclick="editON($event, localRow.comment , 'comment')"
>
{{ localRow.comment }}
</span>
<input
v-else
ref="editField"
v-model="editingContent"
type="text"
autofocus
class="editable-field px-2"
@blur="editOFF"
>
</div>
<div class="td">
<template v-if="fieldType.collation">
<span
v-if="!isInlineEditor.collation"
class="cell-content"
@dblclick="editON($event, localRow.collation, 'collation')"
>
{{ localRow.collation }}
</span>
<select
v-else
ref="editField"
v-model="editingContent"
class="editable-field px-1"
@blur="editOFF"
>
<option
v-for="collation in collations"
:key="collation.collation"
:selected="localRow.collation === collation.collation"
:value="collation.collation"
>
{{ collation.collation }}
</option>
</select>
</template>
</div>
<ConfirmModal
v-if="isDefaultModal"
:confirm-text="$t('word.confirm')"
size="400"
@confirm="editOFF"
@hide="hideDefaultModal"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.default') }} "{{ row.name }}"
</div>
</template>
<div :slot="'body'">
<form class="form-horizontal">
<div class="mb-2">
<label class="form-radio form-inline">
<input
v-model="defaultValue.type"
type="radio"
name="default"
value="noval"
><i class="form-icon" /> No value
</label>
</div>
<div class="mb-2">
<div class="form-group">
<label class="form-radio form-inline col-4">
<input
v-model="defaultValue.type"
value="custom"
type="radio"
name="default"
><i class="form-icon" /> {{ $t('message.customValue') }}
</label>
<div class="column">
<input
v-model="defaultValue.custom"
:disabled="defaultValue.type !== 'custom'"
class="form-input"
type="text"
>
</div>
</div>
</div>
<div class="mb-2">
<label class="form-radio form-inline">
<input
v-model="defaultValue.type"
type="radio"
name="default"
value="null"
><i class="form-icon" /> NULL
</label>
</div>
<div class="mb-2">
<label class="form-radio form-inline">
<input
v-model="defaultValue.type"
:disabled="localRow.key !== 'pri'"
type="radio"
name="default"
value="autoincrement"
><i class="form-icon" /> AUTO_INCREMENT
</label>
</div>
<div class="mb-2">
<div class="form-group">
<label class="form-radio form-inline col-4">
<input
v-model="defaultValue.type"
type="radio"
name="default"
value="expression"
><i class="form-icon" /> {{ $t('word.expression') }}
</label>
<div class="column">
<input
v-model="defaultValue.expression"
:disabled="defaultValue.type !== 'expression'"
class="form-input"
type="text"
>
</div>
</div>
</div>
<div>
<div class="form-group">
<label class="form-label col-4">
{{ $t('message.onUpdate') }}
</label>
<div class="column">
<input
v-model="defaultValue.onUpdate"
class="form-input"
type="text"
>
</div>
</div>
</div>
</form>
</div>
</ConfirmModal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsTableRow',
components: {
ConfirmModal
},
props: {
row: Object,
dataTypes: Array
},
data () {
return {
localRow: {},
isInlineEditor: {},
isDefaultModal: false,
defaultValue: {
type: 'noval',
custom: '',
expression: '',
onUpdate: ''
},
editingContent: null,
editingField: null
};
},
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected',
getWorkspace: 'workspaces/getWorkspace'
}),
localLength () {
return this.localRow.numLength || this.localRow.charLength || this.localRow.datePrecision || 0;
},
fieldType () {
const fieldType = this.dataTypes.reduce((acc, group) => [...acc, ...group.types], []).filter(type =>
type.name === (this.localRow.type ? this.localRow.type.toUpperCase() : '')
);
const group = this.dataTypes.filter(group => group.types.some(type =>
type.name === (this.localRow.type ? this.localRow.type.toUpperCase() : ''))
);
return fieldType.length ? { ...fieldType[0], group: group[0].group } : {};
},
fieldDefault () {
if (this.localRow.autoIncrement) return 'AUTO_INCREMENT';
if (this.localRow.default === 'NULL') return 'NULL';
return this.localRow.default;
},
collations () {
return this.getWorkspace(this.selectedWorkspace).collations;
}
},
watch: {
localRow () {
this.initLocalRow();
},
row () {
this.localRow = this.row;
}
},
mounted () {
this.localRow = this.row;
this.initLocalRow();
this.isInlineEditor.length = false;
},
methods: {
keyName (key) {
switch (key) {
case 'pri':
return 'PRIMARY';
case 'uni':
return 'UNIQUE';
case 'mul':
return 'INDEX';
default:
return 'UNKNOWN ' + key;
}
},
lowerCase (val) {
if (val)
return val.toLowerCase();
return val;
},
initLocalRow () {
Object.keys(this.localRow).forEach(key => {
this.isInlineEditor[key] = false;
});
this.defaultValue.onUpdate = this.localRow.onUpdate;
if (this.localRow.autoIncrement)
this.defaultValue.type = 'autoincrement';
else if (this.localRow.default === null)
this.defaultValue.type = 'noval';
else if (this.localRow.default === 'NULL')
this.defaultValue.type = 'null';
else if (this.localRow.default.match(/^'.*'$/g)) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default.replace(/(^')|('$)/g, '');
}
else if (!isNaN(this.localRow.default)) {
this.defaultValue.type = 'custom';
this.defaultValue.custom = this.localRow.default;
}
else {
this.defaultValue.type = 'expression';
this.defaultValue.expression = this.localRow.default;
}
},
updateRow () {
this.$emit('input', this.localRow);
},
editON (event, content, field) {
if (field === 'length') {
if (['integer', 'float', 'binary', 'spatial', 'other'].includes(this.fieldType.group)) this.editingField = 'numLength';
if (['string'].includes(this.fieldType.group)) this.editingField = 'charLength';
if (['time'].includes(this.fieldType.group)) this.editingField = 'datePrecision';
}
else
this.editingField = field;
this.editingContent = content;
const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
if (field === 'default')
this.isDefaultModal = true;
else {
this.$nextTick(() => { // Focus on input
event.target.blur();
this.$nextTick(() => document.querySelector('.editable-field').focus());
});
}
},
editOFF () {
this.localRow[this.editingField] = this.editingContent;
if (this.editingField === 'type') {
this.localRow.numLength = false;
this.localRow.charLength = false;
this.localRow.datePrecision = false;
if (this.fieldType.length) {
if (['integer', 'float', 'binary', 'spatial', 'other'].includes(this.fieldType.group)) this.localRow.numLength = 11;
if (['string'].includes(this.fieldType.group)) this.localRow.charLength = 15;
if (['time'].includes(this.fieldType.group)) this.localRow.datePrecision = 0;
}
if (!this.fieldType.collation)
this.localRow.collation = null;
}
if (this.editingField === 'default') {
switch (this.defaultValue.type) {
case 'autoincrement':
this.localRow.autoIncrement = true;
break;
case 'noval':
this.localRow.autoIncrement = false;
this.localRow.default = null;
break;
case 'null':
this.localRow.autoIncrement = false;
this.localRow.default = 'NULL';
break;
case 'custom':
this.localRow.autoIncrement = false;
this.localRow.default = `'${this.defaultValue.custom}'`;
break;
case 'expression':
this.localRow.autoIncrement = false;
this.localRow.default = this.defaultValue.expression;
break;
}
this.localRow.onUpdate = this.defaultValue.onUpdate;
}
Object.keys(this.isInlineEditor).forEach(key => {
this.isInlineEditor = { ...this.isInlineEditor, [key]: false };
});
this.editingContent = null;
this.editingField = null;
},
hideDefaultModal () {
this.isDefaultModal = false;
}
}
};
</script>
<style lang="scss" scoped>
.editable-field {
margin: 0;
border: none;
line-height: 1;
width: 100%;
position: absolute;
left: 0;
right: 0;
}
.row-draggable {
position: relative;
text-align: right;
padding-left: 28px;
cursor: grab;
.row-draggable-icon {
position: absolute;
left: 0;
font-size: 22px;
}
}
.table-column-title {
display: flex;
align-items: center;
}
.form-checkbox {
padding: 0;
margin: 0;
line-height: 1;
min-height: auto;
.form-icon {
top: 0.15rem;
left: calc(50% - 8px);
}
}
.cell-content {
display: block;
min-height: 0.8rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
</style>

View File

@ -133,8 +133,8 @@ export default {
this.affectedCount = 0;
},
onKey (e) {
e.stopPropagation();
if (this.tabUid === this.workspace.selected_tab) {
if (this.isSelected) {
e.stopPropagation();
if (e.key === 'F9')
this.runQuery(this.query);
}

View File

@ -229,9 +229,11 @@ export default {
},
setLocalResults () {
this.resetSort();
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
return { ...item, _id: uidGen() };
}) : [];
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows
? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
return { ...item, _id: uidGen() };
})
: [];
},
resizeResults () {
if (this.$refs.resultTable) {

View File

@ -66,7 +66,7 @@
/>
</div>
<div class="editor-field-info">
<div><b>{{ $t('word.size') }}</b>: {{ editingContent.length }}</div>
<div><b>{{ $t('word.size') }}</b>: {{ editingContent ? editingContent.length : 0 }}</div>
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
</div>
</div>
@ -353,9 +353,7 @@ export default {
this.$nextTick(() => document.querySelector('.editable-field').focus());
});
const obj = {
[field]: true
};
const obj = { [field]: true };
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
},
editOFF () {

View File

@ -186,8 +186,8 @@ export default {
this.isAddModal = false;
},
onKey (e) {
e.stopPropagation();
if (this.workspace.selected_tab === 'data') {
if (this.isSelected) {
e.stopPropagation();
if (e.key === 'F5')
this.reloadTable();
}