feat: foreign keys management

This commit is contained in:
Fabio Di Stasio 2020-12-15 17:08:36 +01:00
parent c5458159d1
commit 206597e5b8
11 changed files with 597 additions and 39 deletions

5
jsconfig.json Normal file
View File

@ -0,0 +1,5 @@
{
"include": [
"./src/renderer/**/*"
]
}

View File

@ -237,17 +237,27 @@ export class MySQLClient extends AntaresCore {
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
const { rows: extras } = await this
.select('*')
.schema('information_schema')
.from('REFERENTIAL_CONSTRAINTS')
.where({ CONSTRAINT_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
.run();
return rows.map(field => {
const extra = extras.find(x => x.CONSTRAINT_NAME === field.CONSTRAINT_NAME);
return {
schema: field.TABLE_SCHEMA,
table: field.TABLE_NAME,
column: field.COLUMN_NAME,
field: field.COLUMN_NAME,
position: field.ORDINAL_POSITION,
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
constraintName: field.CONSTRAINT_NAME,
refSchema: field.REFERENCED_TABLE_SCHEMA,
refTable: field.REFERENCED_TABLE_NAME,
refColumn: field.REFERENCED_COLUMN_NAME
refField: field.REFERENCED_COLUMN_NAME,
onUpdate: extra.UPDATE_RULE,
onDelete: extra.DELETE_RULE
};
});
}
@ -346,6 +356,7 @@ export class MySQLClient extends AntaresCore {
deletions,
changes,
indexChanges,
foreignChanges,
options
} = params;
@ -390,6 +401,11 @@ export class MySQLClient extends AntaresCore {
}
});
// ADD FOREIGN KEYS
foreignChanges.additions.forEach(addition => {
alterColumns.push(`ADD CONSTRAINT \`${addition.constraintName}\` FOREIGN KEY (\`${addition.field}\`) REFERENCES \`${addition.refTable}\` (\`${addition.refField}\`) ON UPDATE ${addition.onUpdate} ON DELETE ${addition.onDelete}`);
});
// CHANGE FIELDS
changes.forEach(change => {
const length = change.numLength || change.charLength || change.datePrecision;
@ -427,6 +443,12 @@ export class MySQLClient extends AntaresCore {
}
});
// CHANGE FOREIGN KEYS
foreignChanges.changes.forEach(change => {
alterColumns.push(`DROP FOREIGN KEY \`${change.oldName}\``);
alterColumns.push(`ADD CONSTRAINT \`${change.constraintName}\` FOREIGN KEY (\`${change.field}\`) REFERENCES \`${change.refTable}\` (\`${change.refField}\`) ON UPDATE ${change.onUpdate} ON DELETE ${change.onDelete}`);
});
// DROP FIELDS
deletions.forEach(deletion => {
alterColumns.push(`DROP COLUMN \`${deletion.name}\``);
@ -440,6 +462,11 @@ export class MySQLClient extends AntaresCore {
alterColumns.push(`DROP INDEX \`${deletion.name}\``);
});
// DROP FOREIGN KEYS
foreignChanges.deletions.forEach(deletion => {
alterColumns.push(`DROP FOREIGN KEY \`${deletion.constraintName}\``);
});
sql += alterColumns.join(', ');
// RENAME

View File

@ -1,10 +1,14 @@
<template>
<select
ref="editField"
class="px-1"
class="form-select pl-1 pr-4"
:class="{'small-select': size === 'small'}"
@change="onChange"
@blur="$emit('blur')"
>
<option v-if="!isValidDefault" :value="value">
{{ value }} - {{ $t('message.invalidDefault') }}
</option>
<option
v-for="row in foreignList"
:key="row.foreignColumn"
@ -30,7 +34,11 @@ export default {
},
props: {
value: [String, Number],
keyUsage: Object
keyUsage: Object,
size: {
type: String,
default: ''
}
},
data () {
return {
@ -40,7 +48,10 @@ export default {
computed: {
...mapGetters({
selectedWorkspace: 'workspaces/getSelected'
})
}),
isValidDefault () {
return this.foreignList.some(foreign => foreign.foreignColumn.toString() === this.value.toString());
}
},
async created () {
let firstTextField;
@ -64,7 +75,7 @@ export default {
try { // Foregn list
const { status, response } = await Tables.getForeignList({
...params,
column: this.keyUsage.refColumn,
column: this.keyUsage.refField,
description: firstTextField
});

View File

@ -150,7 +150,7 @@ export default {
return this.getWorkspace(this.selectedWorkspace);
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
return this.keyUsage.map(key => key.field);
}
},
watch: {
@ -306,7 +306,7 @@ export default {
this.localRow[field] = files[0].path;
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
return this.keyUsage.find(key => key.field === keyName);
},
onKey (e) {
e.stopPropagation();

View File

@ -0,0 +1,416 @@
<template>
<ConfirmModal
:confirm-text="$t('word.confirm')"
size="medium"
@confirm="confirmForeignsChange"
@hide="$emit('hide')"
>
<template :slot="'header'">
<div class="d-flex">
<i class="mdi mdi-24px mdi-key-link mr-1" /> {{ $t('word.foreignKeys') }} "{{ table }}"
</div>
</template>
<div :slot="'body'">
<div class="columns col-gapless">
<div class="column col-5">
<div class="panel" :style="{ height: modalInnerHeight + 'px'}">
<div class="panel-header pt-0 pl-0">
<div class="d-flex">
<button class="btn btn-dark btn-sm d-flex" @click="addForeign">
<span>{{ $t('word.add') }}</span>
<i class="mdi mdi-24px mdi-link-plus ml-1" />
</button>
<button
class="btn btn-dark btn-sm d-flex ml-2 mr-0"
:title="$t('message.clearChanges')"
:disabled="!isChanged"
@click.prevent="clearChanges"
>
<span>{{ $t('word.clear') }}</span>
<i class="mdi mdi-24px mdi-delete-sweep ml-1" />
</button>
</div>
</div>
<div ref="indexesPanel" class="panel-body p-0 pr-1">
<div
v-for="foreign in foreignProxy"
:key="foreign._id"
class="tile tile-centered c-hand mb-1 p-1"
:class="{'selected-foreign': selectedForeignID === foreign._id}"
@click="selectForeign($event, foreign._id)"
>
<div class="tile-icon">
<div>
<i class="mdi mdi-key-link mdi-24px" />
</div>
</div>
<div class="tile-content">
<div class="tile-title">
{{ foreign.constraintName }}
</div>
<small class="tile-subtitle text-gray d-flex">
<i class="mdi mdi-link-variant mr-1" />
<div class="fk-details-wrapper">
<span v-if="foreign.table !== ''" class="fk-details">
<i class="mdi mdi-table mr-1" />
<span>{{ foreign.table }}.{{ foreign.field }}</span>
</span>
<span v-if="foreign.refTable !== ''" class="fk-details">
<i class="mdi mdi-table mr-1" />
<span>{{ foreign.refTable }}.{{ foreign.refField }}</span>
</span>
</div>
</small>
</div>
<div class="tile-action">
<button
class="btn btn-link remove-field p-0 mr-2"
:title="$t('word.delete')"
@click.prevent="removeIndex(foreign._id)"
>
<i class="mdi mdi-close" />
</button>
</div>
</div>
</div>
</div>
</div>
<div class="column col-7 pl-2 editor-col">
<form
v-if="selectedForeignObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group">
<label class="form-label col-3">
{{ $t('word.name') }}
</label>
<div class="column">
<input
v-model="selectedForeignObj.constraintName"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group mb-4">
<label class="form-label col-3">
{{ $tc('word.field', 1) }}
</label>
<div class="fields-list column pt-1">
<label
v-for="(field, i) in fields"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleField(field.name)"
>
<input type="checkbox" :checked="selectedForeignObj.field === field.name">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
<div class="form-group">
<label class="form-label col-3 pt-0">
{{ $t('message.referenceTable') }}
</label>
<div class="column">
<select
v-model="selectedForeignObj.refTable"
class="form-select"
@change="reloadRefFields"
>
<option
v-for="schemaTable in schemaTables"
:key="schemaTable.name"
:value="schemaTable.name"
>
{{ schemaTable.name }}
</option>
</select>
</div>
</div>
<div class="form-group mb-4">
<label class="form-label col-3">
{{ $t('message.referenceField') }}
</label>
<div class="fields-list column pt-1">
<label
v-for="(field, i) in refFields[selectedForeignID]"
:key="`${field.name}-${i}`"
class="form-checkbox m-0"
@click.prevent="toggleRefField(field.name)"
>
<input type="checkbox" :checked="selectedForeignObj.refField === field.name && selectedForeignObj.refTable === field.table">
<i class="form-icon" /> {{ field.name }}
</label>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('message.onUpdate') }}
</label>
<div class="column">
<select v-model="selectedForeignObj.onUpdate" class="form-select">
<option
v-for="action in foreignActions"
:key="action"
:value="action"
>
{{ action }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label col-3">
{{ $t('message.onDelete') }}
</label>
<div class="column">
<select v-model="selectedForeignObj.onDelete" class="form-select">
<option
v-for="action in foreignActions"
:key="action"
:value="action"
>
{{ action }}
</option>
</select>
</div>
</div>
</form>
<div v-if="!foreignProxy.length" class="empty">
<div class="empty-icon">
<i class="mdi mdi-key-link mdi-48px" />
</div>
<p class="empty-title h5">
{{ $t('message.thereAreNoForeign') }}
</p>
<div class="empty-action">
<button class="btn btn-primary" @click="addForeign">
{{ $t('message.createNewForeign') }}
</button>
</div>
</div>
</div>
</div>
</div>
</ConfirmModal>
</template>
<script>
import { mapActions } from 'vuex';
import { uidGen } from 'common/libs/uidGen';
import Tables from '@/ipc-api/Tables';
import ConfirmModal from '@/components/BaseConfirmModal';
export default {
name: 'WorkspacePropsForeignModal',
components: {
ConfirmModal
},
props: {
localKeyUsage: Array,
connection: Object,
table: String,
schema: String,
schemaTables: Array,
fields: Array,
workspace: Object
},
data () {
return {
foreignProxy: [],
isOptionsChanging: false,
selectedForeignID: '',
modalInnerHeight: 400,
refFields: {},
foreignActions: [
'RESTRICT',
'CASCADE',
'SET NULL',
'NO ACTION'
]
};
},
computed: {
selectedForeignObj () {
return this.foreignProxy.find(foreign => foreign._id === this.selectedForeignID);
},
isChanged () {
return JSON.stringify(this.localKeyUsage) !== JSON.stringify(this.foreignProxy);
},
hasPrimary () {
return this.foreignProxy.some(foreign => foreign.type === 'PRIMARY');
}
},
mounted () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (this.foreignProxy.length)
this.resetSelectedID();
if (this.selectedForeignObj)
this.getRefFields();
this.getModalInnerHeight();
window.addEventListener('resize', this.getModalInnerHeight);
},
destroyed () {
window.removeEventListener('resize', this.getModalInnerHeight);
},
methods: {
...mapActions({
addNotification: 'notifications/addNotification'
}),
confirmForeignsChange () {
this.$emit('foreigns-update', this.foreignProxy);
},
selectForeign (event, id) {
if (this.selectedForeignID !== id && !event.target.classList.contains('remove-field'))
this.selectedForeignID = id;
},
getModalInnerHeight () {
const modalBody = document.querySelector('.modal-body');
if (modalBody)
this.modalInnerHeight = modalBody.clientHeight - (parseFloat(getComputedStyle(modalBody).paddingTop) + parseFloat(getComputedStyle(modalBody).paddingBottom));
},
addForeign () {
this.foreignProxy = [...this.foreignProxy, {
_id: uidGen(),
constraintName: `FK_${this.foreignProxy.length + 1}`,
refSchema: this.schema,
table: this.table,
refTable: '',
field: '',
refField: '',
onUpdate: this.foreignActions[0],
onDelete: this.foreignActions[0]
}];
if (this.foreignProxy.length === 1)
this.resetSelectedID();
setTimeout(() => {
this.$refs.indexesPanel.scrollTop = this.$refs.indexesPanel.scrollHeight + 60;
}, 20);
},
removeIndex (id) {
this.foreignProxy = this.foreignProxy.filter(foreign => foreign._id !== id);
if (this.selectedForeignID === id && this.foreignProxy.length)
this.resetSelectedID();
},
clearChanges () {
this.foreignProxy = JSON.parse(JSON.stringify(this.localKeyUsage));
if (!this.foreignProxy.some(foreign => foreign._id === this.selectedForeignID))
this.resetSelectedID();
},
toggleField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID)
foreign.field = field;
return foreign;
});
},
toggleRefField (field) {
this.foreignProxy = this.foreignProxy.map(foreign => {
if (foreign._id === this.selectedForeignID)
foreign.refField = field;
return foreign;
});
},
resetSelectedID () {
this.selectedForeignID = this.foreignProxy.length ? this.foreignProxy[0]._id : '';
},
async getRefFields () {
const params = {
uid: this.connection.uid,
schema: this.selectedForeignObj.refSchema,
table: this.selectedForeignObj.refTable
};
try { // Field data
const { status, response } = await Tables.getTableColumns(params);
if (status === 'success') {
this.refFields = {
...this.refFields,
[this.selectedForeignID]: response
};
}
else
this.addNotification({ status: 'error', message: response });
}
catch (err) {
this.addNotification({ status: 'error', message: err.stack });
}
},
reloadRefFields () {
this.selectedForeignObj.refField = '';
this.getRefFields();
}
}
};
</script>
<style lang="scss" scoped>
.tile {
border-radius: 2px;
opacity: 0.5;
transition: background 0.2s;
transition: opacity 0.2s;
.tile-action {
opacity: 0;
transition: opacity 0.2s;
}
&:hover {
background: $bg-color-light;
.tile-action {
opacity: 1;
}
}
&.selected-foreign {
background: $bg-color-light;
opacity: 1;
}
}
.editor-col {
border-left: 2px solid $bg-color-light;
}
.fields-list {
max-height: 80px;
overflow: auto;
}
.remove-field .mdi {
pointer-events: none;
}
.fk-details-wrapper {
max-width: calc(100% - 1rem);
.fk-details {
display: flex;
line-height: 1;
align-items: baseline;
> span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
padding-bottom: 2px;
}
}
}
</style>

View File

@ -65,37 +65,45 @@
</div>
<div class="column col-7 pl-2 editor-col">
<form v-if="selectedIndexObj" :style="{ height: modalInnerHeight + 'px'}">
<form
v-if="selectedIndexObj"
:style="{ height: modalInnerHeight + 'px'}"
class="form-horizontal"
>
<div class="form-group">
<label class="form-label">
<label class="form-label col-3">
{{ $t('word.name') }}
</label>
<input
v-model="selectedIndexObj.name"
class="form-input"
type="text"
>
<div class="column">
<input
v-model="selectedIndexObj.name"
class="form-input"
type="text"
>
</div>
</div>
<div class="form-group">
<label class="form-label">
<label class="form-label col-3">
{{ $t('word.type') }}
</label>
<select v-model="selectedIndexObj.type" class="form-select">
<option
v-for="index in indexTypes"
:key="index"
:value="index"
:disabled="index === 'PRIMARY' && hasPrimary"
>
{{ index }}
</option>
</select>
<div class="column">
<select v-model="selectedIndexObj.type" class="form-select">
<option
v-for="index in indexTypes"
:key="index"
:value="index"
:disabled="index === 'PRIMARY' && hasPrimary"
>
{{ index }}
</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">
<label class="form-label col-3">
{{ $tc('word.field', fields.length) }}
</label>
<div class="fields-list">
<div class="fields-list column pt-1">
<label
v-for="(field, i) in fields"
:key="`${field.name}-${i}`"
@ -266,7 +274,7 @@ export default {
}
.fields-list {
max-height: 200px;
max-height: 300px;
overflow: auto;
}

View File

@ -40,7 +40,7 @@
<span>{{ $t('word.indexes') }}</span>
<i class="mdi mdi-24px mdi-key mdi-rotate-45 ml-1" />
</button>
<button class="btn btn-dark btn-sm">
<button class="btn btn-dark btn-sm" @click="showForeignModal">
<span>{{ $t('word.foreignKeys') }}</span>
<i class="mdi mdi-24px mdi-key-link ml-1" />
</button>
@ -57,6 +57,7 @@
ref="indexTable"
:fields="localFields"
:indexes="localIndexes"
:foreigns="localKeyUsage"
:tab-uid="tabUid"
:conn-uid="connection.uid"
:index-types="workspace.indexTypes"
@ -86,6 +87,18 @@
@hide="hideIndexesModal"
@indexes-update="indexesUpdate"
/>
<WorkspacePropsForeignModal
v-if="isForeignModal"
:local-key-usage="localKeyUsage"
:connection="connection"
:table="table"
:schema="schema"
:schema-tables="schemaTables"
:fields="localFields"
:workspace="workspace"
@hide="hideForeignModal"
@foreigns-update="foreignsUpdate"
/>
</div>
</template>
@ -96,13 +109,15 @@ import Tables from '@/ipc-api/Tables';
import WorkspacePropsTable from '@/components/WorkspacePropsTable';
import WorkspacePropsOptionsModal from '@/components/WorkspacePropsOptionsModal';
import WorkspacePropsIndexesModal from '@/components/WorkspacePropsIndexesModal';
import WorkspacePropsForeignModal from '@/components/WorkspacePropsForeignModal';
export default {
name: 'WorkspacePropsTab',
components: {
WorkspacePropsTable,
WorkspacePropsOptionsModal,
WorkspacePropsIndexesModal
WorkspacePropsIndexesModal,
WorkspacePropsForeignModal
},
props: {
connection: Object,
@ -115,6 +130,7 @@ export default {
isSaving: false,
isOptionsModal: false,
isIndexesModal: false,
isForeignModal: false,
isOptionsChanging: false,
originalFields: [],
localFields: [],
@ -148,6 +164,13 @@ export default {
schema () {
return this.workspace.breadcrumbs.schema;
},
schemaTables () {
const schemaTables = this.workspace.structure
.filter(schema => schema.name === this.schema)
.map(schema => schema.tables);
return schemaTables.length ? schemaTables[0].filter(table => table.type === 'table') : [];
},
isChanged () {
return JSON.stringify(this.originalFields) !== JSON.stringify(this.localFields) ||
JSON.stringify(this.originalKeyUsage) !== JSON.stringify(this.localKeyUsage) ||
@ -242,8 +265,13 @@ export default {
const { status, response } = await Tables.getKeyUsage(params);
if (status === 'success') {
this.originalKeyUsage = response;
this.localKeyUsage = JSON.parse(JSON.stringify(response));
this.originalKeyUsage = response.map(foreign => {
return {
_id: uidGen(),
...foreign
};
});
this.localKeyUsage = JSON.parse(JSON.stringify(this.originalKeyUsage));
}
else
this.addNotification({ status: 'error', message: response });
@ -321,6 +349,35 @@ export default {
// Index Deletions
indexChanges.deletions = this.originalIndexes.filter(index => !localIndexIDs.includes(index._id));
// FOREIGN KEYS
const foreignChanges = {
additions: [],
changes: [],
deletions: []
};
const originalForeignIDs = this.originalKeyUsage.reduce((acc, curr) => [...acc, curr._id], []);
const localForeignIDs = this.localKeyUsage.reduce((acc, curr) => [...acc, curr._id], []);
// Foreigns Additions
foreignChanges.additions = this.localKeyUsage.filter(foreign => !originalForeignIDs.includes(foreign._id));
// Foreigns Changes
this.originalKeyUsage.forEach(originalForeign => {
const lI = this.localKeyUsage.findIndex(localForeign => localForeign._id === originalForeign._id);
if (JSON.stringify(originalForeign) !== JSON.stringify(this.localKeyUsage[lI])) {
if (this.localKeyUsage[lI]) {
foreignChanges.changes.push({
...this.localKeyUsage[lI],
oldName: originalForeign.constraintName
});
}
}
});
// Foreigns Deletions
foreignChanges.deletions = this.originalKeyUsage.filter(foreign => !localForeignIDs.includes(foreign._id));
// ALTER
const params = {
uid: this.connection.uid,
schema: this.schema,
@ -329,10 +386,11 @@ export default {
changes,
deletions,
indexChanges,
foreignChanges,
options
};
try { // Key usage (foreign keys)
try {
const { status, response } = await Tables.alterTable(params);
if (status === 'success') {
@ -423,6 +481,15 @@ export default {
},
indexesUpdate (indexes) {
this.localIndexes = indexes;
},
showForeignModal () {
this.isForeignModal = true;
},
hideForeignModal () {
this.isForeignModal = false;
},
foreignsUpdate (foreigns) {
this.localKeyUsage = foreigns;
}
}
};

View File

@ -85,7 +85,7 @@
</div>
</div>
<div class="th">
<div class="column-resizable">
<div class="column-resizable min-100">
<div class="table-column-title">
{{ $t('word.collation') }}
</div>
@ -104,6 +104,7 @@
:key="row._id"
:row="row"
:indexes="getIndexes(row.name)"
:foreigns="getForeigns(row.name)"
:data-types="dataTypes"
@contextmenu="contextMenu"
/>
@ -128,6 +129,7 @@ export default {
props: {
fields: Array,
indexes: Array,
foreigns: Array,
indexTypes: Array,
tabUid: [String, Number],
connUid: String,
@ -214,6 +216,13 @@ export default {
acc.push(...curr.fields.map(f => ({ name: f, type: curr.type })));
return acc;
}, []).filter(f => f.name === field);
},
getForeigns (field) {
return this.foreigns.reduce((acc, curr) => {
if (curr.field === field)
acc.push(`${curr.refTable}.${curr.refField}`);
return acc;
}, []);
}
}
};

View File

@ -15,6 +15,12 @@
class="d-inline-block mdi mdi-key column-key c-help"
:class="`key-${index.type}`"
/>
<i
v-for="foreign in foreigns"
:key="foreign"
:title="foreign"
class="d-inline-block mdi mdi-key-link c-help"
/>
</div>
</div>
<div class="td" tabindex="0">
@ -287,7 +293,8 @@ export default {
props: {
row: Object,
dataTypes: Array,
indexes: Array
indexes: Array,
foreigns: Array
},
data () {
return {

View File

@ -20,6 +20,7 @@
class="editable-field"
:value.sync="editingContent"
:key-usage="getKeyUsage(cKey)"
size="small"
@blur="editOFF"
/>
<template v-else>
@ -256,7 +257,7 @@ export default {
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
},
foreignKeys () {
return this.keyUsage.map(key => key.column);
return this.keyUsage.map(key => key.field);
},
isEditable () {
return this.fields ? !!(this.fields[0].schema && this.fields[0].table) : false;
@ -422,7 +423,7 @@ export default {
this.$emit('select-row', event, row);
},
getKeyUsage (keyName) {
return this.keyUsage.find(key => key.column === keyName);
return this.keyUsage.find(key => key.field === keyName);
}
}
};

View File

@ -117,7 +117,14 @@ module.exports = {
emptyCorfirm: 'Do you confirm to empty',
unsavedChanges: 'Unsaved changes',
discardUnsavedChanges: 'You have some unsaved changes. By leaving this tab these changes will be discarded.',
thereAreNoIndexes: 'There are no indexes'
thereAreNoIndexes: 'There are no indexes',
thereAreNoForeign: 'There are no foreign keys',
createNewForeign: 'Create new foreign key',
referenceTable: 'Ref. table',
referenceField: 'Ref. field',
foreignFields: 'Foreign fields',
invalidDefault: 'Invalid default',
onDelete: 'On delete'
},
// Date and Time
short: {